From 5804bec6d09c47bef4bf0b067f0852d350cbfb04 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Wed, 24 Jun 2026 18:51:39 +0200 Subject: [PATCH 1/6] (script) Copy fork-project from jb-main ``` mkdir fork-project cp -r .run buildSrc gradle build.gradle settings.gradle gradlew gradlew.bat gradle.properties redirectversions.toml fork-project/ ``` --- fork-project/.run/desktop/run1.run.xml | 23 + fork-project/.run/desktop/run1rtl.run.xml | 23 + fork-project/.run/desktop/run3.run.xml | 23 + fork-project/.run/desktop/run4.run.xml | 23 + fork-project/.run/desktop/runFont.run.xml | 25 + .../.run/desktop/runmouseclicks.run.xml | 23 + fork-project/.run/desktop/test.run.xml | 24 + .../.run/mpp/demo/desktop-Demo.run.xml | 24 + .../.run/mpp/demo/desktop-Example1.run.xml | 24 + .../.run/mpp/demo/desktop-ImageViewer.run.xml | 24 + fork-project/.run/mpp/demo/js-Demo.run.xml | 39 + .../.run/mpp/demo/macos-arm64-Demo.run.xml | 24 + fork-project/.run/mpp/demo/wasm-Demo.run.xml | 23 + fork-project/build.gradle | 37 + fork-project/buildSrc/OWNERS | 13 + fork-project/buildSrc/README.md | 30 + .../allowedLicenses/Apache-2.0/LICENSE.txt | 178 + .../allowedLicenses/BSD-3-Clause/LICENSE.txt | 25 + .../applyAndroidXComposeImplPlugin.gradle | 9 + .../apply/applyAndroidXDocsImplPlugin.gradle | 9 + .../apply/applyAndroidXImplPlugin.gradle | 9 + ...plyAndroidXPlaygroundRootImplPlugin.gradle | 9 + .../applyAndroidXRepackageImplPlugin.gradle | 9 + .../apply/applyAndroidXRootImplPlugin.gradle | 9 + .../applyJetBrainsAndroidXImplPlugin.gradle | 9 + ...pplyJetBrainsAndroidXRootImplPlugin.gradle | 9 + .../blank-proguard-rules/proguard-rules.pro | 1 + .../buildSrc/blank-res-api/public.txt | 0 fork-project/buildSrc/build.gradle | 32 + fork-project/buildSrc/gradlew | 234 + fork-project/buildSrc/imports/README.md | 3 + .../build.gradle | 30 + .../benchmark-darwin-plugin/build.gradle | 20 + .../benchmark-gradle-plugin/build.gradle | 16 + .../build.gradle | 36 + .../glance-layout-generator/build.gradle | 6 + .../inspection-gradle-plugin/build.gradle | 17 + .../imports/room-gradle-plugin/build.gradle | 17 + .../stableaidl-gradle-plugin/build.gradle | 15 + .../buildSrc/jetpad-integration/build.gradle | 10 + .../build/jetpad/LibraryBuildInfoFile.java | 92 + .../buildSrc/karmaconfig/karma.conf.js | 30 + .../buildSrc/kotlin-dsl-dependency.gradle | 35 + fork-project/buildSrc/lint/lint.xml | 54 + fork-project/buildSrc/lint/lint_samples.xml | 39 + fork-project/buildSrc/ndk.gradle | 3 + fork-project/buildSrc/plugins/README.md | 5 + fork-project/buildSrc/plugins/build.gradle | 20 + .../androidx/build/AndroidXComposePlugin.kt | 32 + .../build/AndroidXPlaygroundRootPlugin.kt | 40 + .../kotlin/androidx/build/AndroidXPlugin.kt | 47 + .../androidx/build/AndroidXRepackagePlugin.kt | 38 + .../androidx/build/AndroidXRootPlugin.kt | 38 + .../androidx/build/docs/AndroidXDocsPlugin.kt | 39 + .../androidx/build/JetBrainsAndroidXPlugin.kt | 32 + .../build/JetBrainsAndroidXRootPlugin.kt | 35 + .../AndroidXComposePlugin.properties | 17 + .../AndroidXDocsPlugin.properties | 17 + .../AndroidXPlaygroundRootPlugin.properties | 17 + .../gradle-plugins/AndroidXPlugin.properties | 17 + .../AndroidXRepackagePlugin.properties | 17 + .../AndroidXRootPlugin.properties | 17 + .../JetBrainsAndroidXPlugin.properties | 17 + .../JetBrainsAndroidXRootPlugin.properties | 17 + fork-project/buildSrc/private/README.md | 7 + fork-project/buildSrc/private/build.gradle | 12 + .../build/AndroidXComposeImplPlugin.kt | 264 + .../build/AndroidXComposeLintIssues.kt | 49 + .../build/AndroidXGradleProperties.kt | 222 + .../androidx/build/AndroidXImplPlugin.kt | 1661 +++ .../build/AndroidXMultiplatformExtension.kt | 1054 ++ .../build/AndroidXPlaygroundRootImplPlugin.kt | 242 + .../build/AndroidXRepackageImplPlugin.kt | 154 + .../androidx/build/AndroidXRootImplPlugin.kt | 250 + .../androidx/build/AttestationManifestTask.kt | 71 + .../androidx/build/BenchmarkConfiguration.kt | 69 + .../androidx/build/BuildOnServerTask.kt | 56 + .../build/CheckKotlinApiTargetTask.kt | 90 + .../kotlin/androidx/build/ClasspathBuilder.kt | 30 + .../androidx/build/ConfigureAarAsJar.kt | 53 + .../kotlin/androidx/build/CreateYarnRcTask.kt | 60 + .../DependencyAnalysisPostProcessingTasks.kt | 282 + .../androidx/build/DevelocityTokenFetcher.kt | 73 + .../androidx/build/ErrorProneConfiguration.kt | 323 + .../androidx/build/FilteredAnchorTask.kt | 110 + .../main/kotlin/androidx/build/FtlRunner.kt | 333 + .../build/GradleTransformWorkaround.kt | 72 + .../androidx/build/InspectionRelease.kt | 53 + .../main/kotlin/androidx/build/JavaFormat.kt | 104 + .../androidx/build/KonanPrebuiltsSetup.kt | 100 + .../src/main/kotlin/androidx/build/Ktfmt.kt | 292 + .../androidx/build/LibraryVersionsService.kt | 193 + .../androidx/build/LintConfiguration.kt | 313 + .../build/ListAffectedProjectsTask.kt | 172 + .../build/ListAndroidXPropertiesTask.kt | 36 + .../androidx/build/ListProjectsService.kt | 50 + .../androidx/build/ListTaskOutputsTask.kt | 247 + .../androidx/build/MavenUploadHelper.kt | 577 + .../kotlin/androidx/build/MaxDepVersions.kt | 42 + .../build/PrintProjectCoordinatesTask.kt | 108 + .../androidx/build/ProguardConfiguration.kt | 107 + .../androidx/build/ProjectConfigValidators.kt | 122 + .../androidx/build/ProjectCreatorTask.kt | 675 + .../main/kotlin/androidx/build/ProjectExt.kt | 67 + .../kotlin/androidx/build/ProjectParser.kt | 79 + .../kotlin/androidx/build/ProjectResolver.kt | 54 + .../kotlin/androidx/build/PublishingHelper.kt | 41 + .../src/main/kotlin/androidx/build/Release.kt | 227 + .../src/main/kotlin/androidx/build/Samples.kt | 93 + .../kotlin/androidx/build/SettingsParser.kt | 70 + .../main/kotlin/androidx/build/StringUtils.kt | 23 + .../androidx/build/UnpackedStubAarTask.kt | 59 + .../androidx/build/UnzipChromeBuildService.kt | 69 + .../build/ValidateKotlinModuleFiles.kt | 84 + .../build/VerifyDependencyVersionsTask.kt | 276 + .../build/VerifyELFRegionAlignmentTask.kt | 57 + .../build/VerifyLicenseAndVersionFilesTask.kt | 109 + .../build/VerifyRelocatedDependenciesTask.kt | 99 + .../androidx/build/VersionFileWriterTask.kt | 117 + .../main/kotlin/androidx/build/XmlParser.kt | 78 + .../BinaryCompatibilityValidation.kt | 401 + .../CheckAbiEquivalenceTask.kt | 112 + .../CheckAbiIsCompatibleTask.kt | 186 + .../GenerateAbiTask.kt | 119 + .../IgnoreAbiChangesTask.kt | 126 + .../UpdateAbiTask.kt | 116 + ...CreateAggregateLibraryBuildInfoFileTask.kt | 115 + .../CreateLibraryBuildInfoFileTask.kt | 579 + .../build/buildInfo/VariantPublishPlan.kt | 37 + .../androidx/build/checkapi/ApiLocation.kt | 209 + .../androidx/build/checkapi/ApiTasks.kt | 213 + .../androidx/build/checkapi/CheckApi.kt | 146 + .../build/checkapi/CompilationInputs.kt | 328 + .../androidx/build/clang/AndroidXClang.kt | 43 + .../androidx/build/clang/ClangArchiveTask.kt | 82 + .../androidx/build/clang/ClangCompileTask.kt | 91 + .../androidx/build/clang/ClangLinkerTask.kt | 104 + .../build/clang/CombineObjectFilesTask.kt | 156 + .../clang/CreateDefFileWithLibraryPathTask.kt | 75 + .../androidx/build/clang/KonanBuildService.kt | 260 + .../androidx/build/clang/KonanCinteropExt.kt | 133 + .../clang/MultiTargetNativeCompilation.kt | 287 + .../build/clang/NativeLibraryBundler.kt | 116 + .../build/clang/NativeTargetCompilation.kt | 148 + .../kotlin/androidx/build/clang/README.md | 47 + .../build/clang/SerializableKonanTarget.kt | 42 + .../androidx/build/dackka/DackkaTask.kt | 401 + .../androidx/build/dackka/DokkaInputModels.kt | 64 + .../androidx/build/dackka/DokkaUtils.kt | 86 + .../build/dackka/GenerateMetadataTask.kt | 128 + .../androidx/build/dackka/MetadataEntry.kt | 27 + .../main/kotlin/androidx/build/dackka/OWNERS | 3 + .../AffectedModuleDetector.kt | 553 + .../dependencyTracker/BuildPropParser.kt | 90 + .../dependencyTracker/DependencyTracker.kt | 52 + .../build/dependencyTracker/FileLogger.kt | 53 + .../build/dependencyTracker/ProjectGraph.kt | 97 + .../build/dependencyTracker/ToStringLogger.kt | 36 + .../DependencyAllowlist.kt | 71 + .../build/docs/AndroidXDocsImplPlugin.kt | 865 ++ .../build/docs/CheckTipOfTreeDocsTask.kt | 133 + .../main/kotlin/androidx/build/docs/OWNERS | 3 + .../androidx/build/gitclient/ChangeInfo.kt | 173 + .../androidx/build/gitclient/GitClient.kt | 148 + .../build/kythe/GenerateJavaKzipTask.kt | 181 + .../build/kythe/GenerateKotlinKzipTask.kt | 260 + .../kotlin/androidx/build/kythe/KzipTasks.kt | 77 + .../androidx/build/license/AddLicenses.kt | 107 + .../license/ValidateLicensesExistTask.kt | 72 + .../androidx/build/lint/ValidateLintChecks.kt | 43 + .../kotlin/androidx/build/logging/logging.kt | 20 + .../metalava/CheckApiCompatibilityTask.kt | 102 + .../build/metalava/CheckApiEquivalenceTask.kt | 109 + .../build/metalava/GenerateApiLevels.kt | 119 + .../build/metalava/GenerateApiTask.kt | 105 + .../androidx/build/metalava/MetalavaRunner.kt | 483 + .../androidx/build/metalava/MetalavaTask.kt | 225 + .../androidx/build/metalava/MetalavaTasks.kt | 250 + .../androidx/build/metalava/ProjectXml.kt | 230 + .../build/metalava/RegenerateOldApisTask.kt | 306 + .../androidx/build/metalava/UpdateApiTask.kt | 148 + .../build/metalava/UpdateBaselineTasks.kt | 125 + .../kotlin/androidx/build/playground/OWNERS | 2 + .../playground/ValidateIntegrationPatches.kt | 87 + ...VerifyPlaygroundGradleConfigurationTask.kt | 204 + .../resources/CheckResourceApiReleaseTask.kt | 93 + .../build/resources/CheckResourceApiTask.kt | 57 + .../resources/CopyPublicResourcesDirTask.kt | 56 + .../resources/GenerateResourceApiTask.kt | 85 + .../resources/PublicResourcesStubHelper.kt | 34 + .../androidx/build/resources/ResourceTasks.kt | 121 + .../build/resources/UpdateResourceApiTask.kt | 84 + .../androidx/build/sbom/ExportSbomsTask.kt | 59 + .../main/kotlin/androidx/build/sbom/Sbom.kt | 351 + .../build/sources/SourceJarTaskHelper.kt | 347 + .../ValidateMultiplatformSourceSetNaming.kt | 147 + .../build/stableaidl/StableAidlApiTasks.kt | 59 + .../build/studio/StudioPlatformUtilities.kt | 209 + .../androidx/build/studio/StudioTask.kt | 484 + .../AndroidTestConfigBuilder.kt | 368 + .../build/testConfiguration/AppApksModel.kt | 66 + .../AppApksTestConfigurationHelper.kt | 40 + .../CopyApkFromArtifactsTask.kt | 94 + .../testConfiguration/CopyTestApksTask.kt | 72 + .../GenerateTestConfigurationTask.kt | 173 + .../build/testConfiguration/OwnersService.kt | 92 + .../testConfiguration/TestApkSha256Report.kt | 32 + .../testConfiguration/TestSourceSetsHelper.kt | 67 + .../TestSuiteConfiguration.kt | 319 + .../build/uptodatedness/EnableCaching.kt | 33 + .../uptodatedness/TaskUpToDateValidator.kt | 271 + .../build/AndroidXForkTargetsExtensions.kt | 211 + .../androidx/build/ArtifactRedirection.kt | 255 + .../build/JetBrainsAndroidXImplPlugin.kt | 134 + ...nsAndroidXRedirectingPublicationHelpers.kt | 0 .../build/JetBrainsAndroidXRootImplPlugin.kt | 68 + .../androidx/build/JetBrainsCapabilityRule.kt | 211 + .../JetBrainsCompatibilityVersionsExt.kt | 42 + .../build/JetBrainsMavenCoordinatesChanger.kt | 40 + .../JetBrainsVerifyDependencyVersionsTask.kt | 149 + .../androidx/build/MavenUploadHelper.kt | 723 + .../AndroidXComposeImplPlugin.properties | 17 + .../AndroidXDocsImplPlugin.properties | 17 + .../AndroidXImplPlugin.properties | 17 + ...ndroidXPlaygroundRootImplPlugin.properties | 17 + .../AndroidXRootImplPlugin.properties | 17 + fork-project/buildSrc/public/README.md | 3 + fork-project/buildSrc/public/build.gradle | 1 + .../kotlin/androidx/build/AndroidXConfig.kt | 141 + .../androidx/build/AndroidXConfiguration.kt | 55 + .../androidx/build/AndroidXExtension.kt | 528 + .../build/AndroidXPublicGradleProperties.kt | 22 + .../kotlin/androidx/build/ApkCopyHelper.kt | 103 + .../kotlin/androidx/build/BuildOnServer.kt | 33 + .../build/BuildServerConfiguration.kt | 101 + .../androidx/build/BundleInsideHelper.kt | 193 + .../ExportAtomicLibraryGroupsToTextTask.kt | 57 + .../kotlin/androidx/build/IncludedProject.kt | 28 + .../kotlin/androidx/build/KmpPlatforms.kt | 145 + .../kotlin/androidx/build/LibraryGroup.kt | 32 + .../kotlin/androidx/build/OperatingSystem.kt | 38 + .../kotlin/androidx/build/ProjectIsolation.kt | 23 + .../androidx/build/ProjectLayoutType.kt | 51 + .../androidx/build/ProjectOrArtifact.kt | 60 + .../androidx/build/RobolectricHelper.kt | 123 + .../main/kotlin/androidx/build/SdkHelper.kt | 111 + .../androidx/build/SdkResourceGenerator.kt | 172 + .../kotlin/androidx/build/SingleFileCopy.kt | 43 + .../kotlin/androidx/build/SoftwareType.kt | 387 + .../src/main/kotlin/androidx/build/Version.kt | 155 + .../build/VersionCatalogExtensions.kt | 44 + .../androidx/build/gradle/Extensions.kt | 37 + .../androidx/build/ComposeComponent.kt | 11 + .../androidx/build/ComposePlatforms.kt | 128 + .../androidx/build/ComposeProperties.kt | 14 + .../androidx/build/ComposePublishingTask.kt | 92 + .../build/GenerateNotoFontFallbackDataTask.kt | 731 + .../build/JetBrainsCompatibilityVersions.kt | 23 + .../androidx/build/JetBrainsPublication.kt | 210 + .../build/JetBrainsVersionsService.kt | 58 + .../androidx/build/UpdateTranslationsTask.kt | 401 + .../androidx/build/XcodeBuildLock.kt | 43 + fork-project/buildSrc/repos.gradle | 103 + fork-project/buildSrc/res/values/public.xml | 21 + fork-project/buildSrc/settings.gradle | 59 + .../buildSrc/settingsScripts/out-setup.groovy | 42 + .../project-dependency-graph.groovy | 395 + .../settingsScripts/skiko-setup.groovy | 65 + .../buildSrc/shared-dependencies.gradle | 93 + fork-project/buildSrc/shared.gradle | 54 + .../buildSrc/src/main/resources/README.md | 9 + .../android/app/Notification.aidl | 19 + .../android/app/PendingIntent.aidl | 19 + .../android/content/ComponentName.aidl | 19 + .../android/content/Intent.aidl | 19 + .../content/res/AssetFileDescriptor.aidl | 19 + .../android/content/res/Configuration.aidl | 19 + .../android/graphics/Bitmap.aidl | 19 + .../android/graphics/Insets.aidl | 19 + .../android/graphics/Rect.aidl | 19 + .../pdf/content/PdfPageGotoLinkContent.aidl | 3 + .../pdf/content/PdfPageImageContent.aidl | 3 + .../pdf/content/PdfPageLinkContent.aidl | 3 + .../pdf/content/PdfPageTextContent.aidl | 3 + .../graphics/pdf/models/FormEditRecord.aidl | 3 + .../graphics/pdf/models/FormWidgetInfo.aidl | 3 + .../graphics/pdf/models/PageMatchBounds.aidl | 3 + .../pdf/models/selection/PageSelection.aidl | 3 + .../models/selection/SelectionBoundary.aidl | 3 + .../android/location/Location.aidl | 19 + .../stableAidlImports/android/net/Uri.aidl | 19 + .../stableAidlImports/android/os/Bundle.aidl | 19 + .../stableAidlImports/android/os/IBinder.aidl | 19 + .../android/view/KeyEvent.aidl | 19 + .../android/view/MotionEvent.aidl | 19 + .../android/view/Surface.aidl | 19 + .../view/inputmethod/CompletionInfo.aidl | 19 + .../view/inputmethod/CorrectionInfo.aidl | 19 + .../android/view/inputmethod/EditorInfo.aidl | 19 + .../view/inputmethod/ExtractedText.aidl | 19 + .../inputmethod/ExtractedTextRequest.aidl | 19 + fork-project/buildSrc/vnames.json | 35 + fork-project/gradle.properties | 132 + fork-project/gradle/OWNERS | 2 + fork-project/gradle/README.md | 59 + fork-project/gradle/libs.versions.toml | 342 + fork-project/gradle/verification-keyring.keys | 10978 ++++++++++++++++ fork-project/gradle/verification-metadata.xml | 840 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes .../gradle/wrapper/gradle-wrapper.jar.asc | 16 + .../gradle/wrapper/gradle-wrapper.properties | 6 + fork-project/gradlew | 248 + fork-project/gradlew.bat | 93 + .../redirectversions.toml | 0 fork-project/settings.gradle | 604 + 315 files changed, 47418 insertions(+) create mode 100644 fork-project/.run/desktop/run1.run.xml create mode 100644 fork-project/.run/desktop/run1rtl.run.xml create mode 100644 fork-project/.run/desktop/run3.run.xml create mode 100644 fork-project/.run/desktop/run4.run.xml create mode 100644 fork-project/.run/desktop/runFont.run.xml create mode 100644 fork-project/.run/desktop/runmouseclicks.run.xml create mode 100644 fork-project/.run/desktop/test.run.xml create mode 100644 fork-project/.run/mpp/demo/desktop-Demo.run.xml create mode 100644 fork-project/.run/mpp/demo/desktop-Example1.run.xml create mode 100644 fork-project/.run/mpp/demo/desktop-ImageViewer.run.xml create mode 100644 fork-project/.run/mpp/demo/js-Demo.run.xml create mode 100644 fork-project/.run/mpp/demo/macos-arm64-Demo.run.xml create mode 100644 fork-project/.run/mpp/demo/wasm-Demo.run.xml create mode 100644 fork-project/build.gradle create mode 100644 fork-project/buildSrc/OWNERS create mode 100644 fork-project/buildSrc/README.md create mode 100644 fork-project/buildSrc/allowedLicenses/Apache-2.0/LICENSE.txt create mode 100644 fork-project/buildSrc/allowedLicenses/BSD-3-Clause/LICENSE.txt create mode 100644 fork-project/buildSrc/apply/applyAndroidXComposeImplPlugin.gradle create mode 100644 fork-project/buildSrc/apply/applyAndroidXDocsImplPlugin.gradle create mode 100644 fork-project/buildSrc/apply/applyAndroidXImplPlugin.gradle create mode 100644 fork-project/buildSrc/apply/applyAndroidXPlaygroundRootImplPlugin.gradle create mode 100644 fork-project/buildSrc/apply/applyAndroidXRepackageImplPlugin.gradle create mode 100644 fork-project/buildSrc/apply/applyAndroidXRootImplPlugin.gradle create mode 100644 fork-project/buildSrc/apply/applyJetBrainsAndroidXImplPlugin.gradle create mode 100644 fork-project/buildSrc/apply/applyJetBrainsAndroidXRootImplPlugin.gradle create mode 100644 fork-project/buildSrc/blank-proguard-rules/proguard-rules.pro create mode 100644 fork-project/buildSrc/blank-res-api/public.txt create mode 100644 fork-project/buildSrc/build.gradle create mode 100644 fork-project/buildSrc/gradlew create mode 100644 fork-project/buildSrc/imports/README.md create mode 100644 fork-project/buildSrc/imports/baseline-profile-gradle-plugin/build.gradle create mode 100644 fork-project/buildSrc/imports/benchmark-darwin-plugin/build.gradle create mode 100644 fork-project/buildSrc/imports/benchmark-gradle-plugin/build.gradle create mode 100644 fork-project/buildSrc/imports/binary-compatibility-validator/build.gradle create mode 100644 fork-project/buildSrc/imports/glance-layout-generator/build.gradle create mode 100644 fork-project/buildSrc/imports/inspection-gradle-plugin/build.gradle create mode 100644 fork-project/buildSrc/imports/room-gradle-plugin/build.gradle create mode 100644 fork-project/buildSrc/imports/stableaidl-gradle-plugin/build.gradle create mode 100644 fork-project/buildSrc/jetpad-integration/build.gradle create mode 100644 fork-project/buildSrc/jetpad-integration/src/main/java/androidx/build/jetpad/LibraryBuildInfoFile.java create mode 100644 fork-project/buildSrc/karmaconfig/karma.conf.js create mode 100644 fork-project/buildSrc/kotlin-dsl-dependency.gradle create mode 100644 fork-project/buildSrc/lint/lint.xml create mode 100644 fork-project/buildSrc/lint/lint_samples.xml create mode 100644 fork-project/buildSrc/ndk.gradle create mode 100644 fork-project/buildSrc/plugins/README.md create mode 100644 fork-project/buildSrc/plugins/build.gradle create mode 100644 fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXComposePlugin.kt create mode 100644 fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt create mode 100644 fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPlugin.kt create mode 100644 fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXRepackagePlugin.kt create mode 100644 fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt create mode 100644 fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt create mode 100644 fork-project/buildSrc/plugins/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXPlugin.kt create mode 100644 fork-project/buildSrc/plugins/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXRootPlugin.kt create mode 100644 fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXComposePlugin.properties create mode 100644 fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXDocsPlugin.properties create mode 100644 fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPlaygroundRootPlugin.properties create mode 100644 fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPlugin.properties create mode 100644 fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXRepackagePlugin.properties create mode 100644 fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXRootPlugin.properties create mode 100644 fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/JetBrainsAndroidXPlugin.properties create mode 100644 fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/JetBrainsAndroidXRootPlugin.properties create mode 100644 fork-project/buildSrc/private/README.md create mode 100644 fork-project/buildSrc/private/build.gradle create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeLintIssues.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRepackageImplPlugin.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/AttestationManifestTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/BenchmarkConfiguration.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/BuildOnServerTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/CheckKotlinApiTargetTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ClasspathBuilder.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ConfigureAarAsJar.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/CreateYarnRcTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/DependencyAnalysisPostProcessingTasks.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/GradleTransformWorkaround.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/InspectionRelease.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/JavaFormat.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/Ktfmt.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListAffectedProjectsTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListAndroidXPropertiesTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListProjectsService.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/MaxDepVersions.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/PrintProjectCoordinatesTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProguardConfiguration.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectConfigValidators.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectCreatorTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectExt.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/PublishingHelper.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/Release.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/Samples.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/SettingsParser.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/StringUtils.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/UnpackedStubAarTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/UnzipChromeBuildService.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/ValidateKotlinModuleFiles.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyELFRegionAlignmentTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyLicenseAndVersionFilesTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyRelocatedDependenciesTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/XmlParser.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckAbiEquivalenceTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckAbiIsCompatibleTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/GenerateAbiTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/IgnoreAbiChangesTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/VariantPublishPlan.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiLocation.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CheckApi.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/AndroidXClang.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangArchiveTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangCompileTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangLinkerTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/CombineObjectFilesTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/CreateDefFileWithLibraryPathTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanCinteropExt.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/MultiTargetNativeCompilation.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeTargetCompilation.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/README.md create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/SerializableKonanTarget.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DokkaInputModels.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DokkaUtils.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/GenerateMetadataTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/MetadataEntry.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/BuildPropParser.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/DependencyTracker.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/FileLogger.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/ProjectGraph.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyallowlist/DependencyAllowlist.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/CheckTipOfTreeDocsTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/gitclient/ChangeInfo.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/license/AddLicenses.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/license/ValidateLicensesExistTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/lint/ValidateLintChecks.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/logging/logging.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiCompatibilityTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiEquivalenceTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiLevels.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/ProjectXml.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateApiTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateBaselineTasks.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/OWNERS create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/ValidateIntegrationPatches.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradleConfigurationTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CheckResourceApiReleaseTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CheckResourceApiTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CopyPublicResourcesDirTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/GenerateResourceApiTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/ResourceTasks.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/UpdateResourceApiTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/sbom/ExportSbomsTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/sources/SourceJarTaskHelper.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/sources/ValidateMultiplatformSourceSetNaming.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/stableaidl/StableAidlApiTasks.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AppApksModel.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AppApksTestConfigurationHelper.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyApkFromArtifactsTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/OwnersService.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestApkSha256Report.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSourceSetsHelper.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/EnableCaching.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/AndroidXForkTargetsExtensions.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/ArtifactRedirection.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXImplPlugin.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXRedirectingPublicationHelpers.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXRootImplPlugin.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsCapabilityRule.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsCompatibilityVersionsExt.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsMavenCoordinatesChanger.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsVerifyDependencyVersionsTask.kt create mode 100644 fork-project/buildSrc/private/src/main/kotlin/org/jetbrains/androidx/build/MavenUploadHelper.kt create mode 100644 fork-project/buildSrc/private/src/main/resources/META-INF/gradle-plugins/AndroidXComposeImplPlugin.properties create mode 100644 fork-project/buildSrc/private/src/main/resources/META-INF/gradle-plugins/AndroidXDocsImplPlugin.properties create mode 100644 fork-project/buildSrc/private/src/main/resources/META-INF/gradle-plugins/AndroidXImplPlugin.properties create mode 100644 fork-project/buildSrc/private/src/main/resources/META-INF/gradle-plugins/AndroidXPlaygroundRootImplPlugin.properties create mode 100644 fork-project/buildSrc/private/src/main/resources/META-INF/gradle-plugins/AndroidXRootImplPlugin.properties create mode 100644 fork-project/buildSrc/public/README.md create mode 100644 fork-project/buildSrc/public/build.gradle create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfig.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/AndroidXConfiguration.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/AndroidXExtension.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/AndroidXPublicGradleProperties.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/ApkCopyHelper.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/BuildOnServer.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/ExportAtomicLibraryGroupsToTextTask.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/IncludedProject.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/LibraryGroup.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/OperatingSystem.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/ProjectIsolation.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/ProjectLayoutType.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/ProjectOrArtifact.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/RobolectricHelper.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/SdkHelper.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/SdkResourceGenerator.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/SingleFileCopy.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/SoftwareType.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/Version.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/VersionCatalogExtensions.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/androidx/build/gradle/Extensions.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/ComposeComponent.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/ComposePlatforms.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/ComposeProperties.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/ComposePublishingTask.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/GenerateNotoFontFallbackDataTask.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsCompatibilityVersions.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsPublication.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsVersionsService.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/UpdateTranslationsTask.kt create mode 100644 fork-project/buildSrc/public/src/main/kotlin/org/jetbrains/androidx/build/XcodeBuildLock.kt create mode 100644 fork-project/buildSrc/repos.gradle create mode 100644 fork-project/buildSrc/res/values/public.xml create mode 100644 fork-project/buildSrc/settings.gradle create mode 100644 fork-project/buildSrc/settingsScripts/out-setup.groovy create mode 100644 fork-project/buildSrc/settingsScripts/project-dependency-graph.groovy create mode 100644 fork-project/buildSrc/settingsScripts/skiko-setup.groovy create mode 100644 fork-project/buildSrc/shared-dependencies.gradle create mode 100644 fork-project/buildSrc/shared.gradle create mode 100644 fork-project/buildSrc/src/main/resources/README.md create mode 100644 fork-project/buildSrc/stableAidlImports/android/app/Notification.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/app/PendingIntent.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/content/ComponentName.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/content/Intent.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/content/res/AssetFileDescriptor.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/content/res/Configuration.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/Bitmap.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/Insets.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/Rect.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/pdf/content/PdfPageGotoLinkContent.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/pdf/content/PdfPageImageContent.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/pdf/content/PdfPageLinkContent.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/pdf/content/PdfPageTextContent.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/pdf/models/FormEditRecord.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/pdf/models/FormWidgetInfo.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/pdf/models/PageMatchBounds.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/pdf/models/selection/PageSelection.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/graphics/pdf/models/selection/SelectionBoundary.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/location/Location.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/net/Uri.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/os/Bundle.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/os/IBinder.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/view/KeyEvent.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/view/MotionEvent.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/view/Surface.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/view/inputmethod/CompletionInfo.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/view/inputmethod/CorrectionInfo.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/view/inputmethod/EditorInfo.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/view/inputmethod/ExtractedText.aidl create mode 100644 fork-project/buildSrc/stableAidlImports/android/view/inputmethod/ExtractedTextRequest.aidl create mode 100644 fork-project/buildSrc/vnames.json create mode 100644 fork-project/gradle.properties create mode 100644 fork-project/gradle/OWNERS create mode 100644 fork-project/gradle/README.md create mode 100644 fork-project/gradle/libs.versions.toml create mode 100644 fork-project/gradle/verification-keyring.keys create mode 100644 fork-project/gradle/verification-metadata.xml create mode 100644 fork-project/gradle/wrapper/gradle-wrapper.jar create mode 100644 fork-project/gradle/wrapper/gradle-wrapper.jar.asc create mode 100644 fork-project/gradle/wrapper/gradle-wrapper.properties create mode 100644 fork-project/gradlew create mode 100644 fork-project/gradlew.bat rename redirectversions.toml => fork-project/redirectversions.toml (100%) create mode 100644 fork-project/settings.gradle diff --git a/fork-project/.run/desktop/run1.run.xml b/fork-project/.run/desktop/run1.run.xml new file mode 100644 index 0000000000000..9992e606cd1c3 --- /dev/null +++ b/fork-project/.run/desktop/run1.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/fork-project/.run/desktop/run1rtl.run.xml b/fork-project/.run/desktop/run1rtl.run.xml new file mode 100644 index 0000000000000..c980c8a247489 --- /dev/null +++ b/fork-project/.run/desktop/run1rtl.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/fork-project/.run/desktop/run3.run.xml b/fork-project/.run/desktop/run3.run.xml new file mode 100644 index 0000000000000..1d582b757bdf2 --- /dev/null +++ b/fork-project/.run/desktop/run3.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/fork-project/.run/desktop/run4.run.xml b/fork-project/.run/desktop/run4.run.xml new file mode 100644 index 0000000000000..00b3d1f25737f --- /dev/null +++ b/fork-project/.run/desktop/run4.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/fork-project/.run/desktop/runFont.run.xml b/fork-project/.run/desktop/runFont.run.xml new file mode 100644 index 0000000000000..48ded718e5d07 --- /dev/null +++ b/fork-project/.run/desktop/runFont.run.xml @@ -0,0 +1,25 @@ + + + + + + + true + true + false + false + + + diff --git a/fork-project/.run/desktop/runmouseclicks.run.xml b/fork-project/.run/desktop/runmouseclicks.run.xml new file mode 100644 index 0000000000000..49e3ad66004f3 --- /dev/null +++ b/fork-project/.run/desktop/runmouseclicks.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/fork-project/.run/desktop/test.run.xml b/fork-project/.run/desktop/test.run.xml new file mode 100644 index 0000000000000..c223f0f318a78 --- /dev/null +++ b/fork-project/.run/desktop/test.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + true + + + \ No newline at end of file diff --git a/fork-project/.run/mpp/demo/desktop-Demo.run.xml b/fork-project/.run/mpp/demo/desktop-Demo.run.xml new file mode 100644 index 0000000000000..69c513c0e2dfd --- /dev/null +++ b/fork-project/.run/mpp/demo/desktop-Demo.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/fork-project/.run/mpp/demo/desktop-Example1.run.xml b/fork-project/.run/mpp/demo/desktop-Example1.run.xml new file mode 100644 index 0000000000000..dbca9a15e50e9 --- /dev/null +++ b/fork-project/.run/mpp/demo/desktop-Example1.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/fork-project/.run/mpp/demo/desktop-ImageViewer.run.xml b/fork-project/.run/mpp/demo/desktop-ImageViewer.run.xml new file mode 100644 index 0000000000000..f6e7121bab0da --- /dev/null +++ b/fork-project/.run/mpp/demo/desktop-ImageViewer.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/fork-project/.run/mpp/demo/js-Demo.run.xml b/fork-project/.run/mpp/demo/js-Demo.run.xml new file mode 100644 index 0000000000000..0f71e22920ad6 --- /dev/null +++ b/fork-project/.run/mpp/demo/js-Demo.run.xml @@ -0,0 +1,39 @@ + + + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/fork-project/.run/mpp/demo/macos-arm64-Demo.run.xml b/fork-project/.run/mpp/demo/macos-arm64-Demo.run.xml new file mode 100644 index 0000000000000..7733ef47ece07 --- /dev/null +++ b/fork-project/.run/mpp/demo/macos-arm64-Demo.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/fork-project/.run/mpp/demo/wasm-Demo.run.xml b/fork-project/.run/mpp/demo/wasm-Demo.run.xml new file mode 100644 index 0000000000000..5c989357053b0 --- /dev/null +++ b/fork-project/.run/mpp/demo/wasm-Demo.run.xml @@ -0,0 +1,23 @@ + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/fork-project/build.gradle b/fork-project/build.gradle new file mode 100644 index 0000000000000..f3b8359cf4042 --- /dev/null +++ b/fork-project/build.gradle @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This file was created using the `create_project.py` script located in the + * `/development/project-creator` directory. + * + * Please use that script when creating a new project, rather than copying an existing project and + * modifying its settings. + */ +import androidx.build.AndroidXRootPlugin +import androidx.build.SdkHelperKt +import org.jetbrains.androidx.build.JetBrainsAndroidXRootPlugin + +buildscript { + SdkHelperKt.setSupportRootFolder(project, project.projectDir) + + // Needed for atomicfu plugin + apply(from: "buildSrc/repos.gradle") + repos.addMavenRepositories(repositories) +} + +apply plugin: AndroidXRootPlugin +apply plugin: JetBrainsAndroidXRootPlugin diff --git a/fork-project/buildSrc/OWNERS b/fork-project/buildSrc/OWNERS new file mode 100644 index 0000000000000..e661d2a5a041d --- /dev/null +++ b/fork-project/buildSrc/OWNERS @@ -0,0 +1,13 @@ +# Bug component: 461356 +set noparent + +aurimas@google.com +alanv@google.com +fsladkey@google.com +omarismail@google.com +radhanakade@google.com + +per-file *AndroidXPlaygroundRootPlugin.kt = dustinlam@google.com, rahulrav@google.com +per-file *LintConfiguration.kt = juliamcclellan@google.com +per-file lint/lint.xml = juliamcclellan@google.com +per-file *AndroidXComposeLintIssues.kt = anbailey@google.com diff --git a/fork-project/buildSrc/README.md b/fork-project/buildSrc/README.md new file mode 100644 index 0000000000000..c4c40a8250d37 --- /dev/null +++ b/fork-project/buildSrc/README.md @@ -0,0 +1,30 @@ +This is the buildSrc project. +Gradle builds (and tests) this project before the other projects, and Gradle adds its artifacts into the classpath of the other projects when configuring them. + +Tests for the buildSrc project are located in the buildSrc-tests project, so that the build doesn't need to wait for those tests to complete + +To run these tests you can run `./gradlew :buildSrc-tests:test` + +For information about Gradle's configuration caching, see: + * https://medium.com/androiddevelopers/configuration-caching-deep-dive-bcb304698070 + * https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution + * https://github.com/gradle/gradle/issues/17813 + +The buildSrc directory is split into multiple projects based on what needs to be available on the classpath when parsing build.gradle files outside of buildSrc. +Any classes that Gradle puts on the classpath for parsing build.gradle files can theoretically overwrite the implementation of tasks in those projects. +So, if a class is on that classpath and it changes, then Gradle is not confident that the task is necessarily up-to-date and Gradle will rerun it. +So, we move as many classes as possible off of this classpath by applying them from within a separate .gradle script instead. + +To verify that classes in private/ don't unnecessarily affect the up-to-datedness status of tasks from outside plugins, try something like this: + +``` + # run a kotlin compilation task + ./gradlew :core:core:compileDebugKotlin + # make some unrelated changes in buildSrc: + sed -i 's/ignoreCase = true/ignoreCase = false/g' buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt + # rerun same kotlin compilation task + ./gradlew :core:core:compileDebugKotlin | cat + # see that the tasks were up-to-date +``` + +See also b/140265324 for more information. diff --git a/fork-project/buildSrc/allowedLicenses/Apache-2.0/LICENSE.txt b/fork-project/buildSrc/allowedLicenses/Apache-2.0/LICENSE.txt new file mode 100644 index 0000000000000..e454a52586f29 --- /dev/null +++ b/fork-project/buildSrc/allowedLicenses/Apache-2.0/LICENSE.txt @@ -0,0 +1,178 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/fork-project/buildSrc/allowedLicenses/BSD-3-Clause/LICENSE.txt b/fork-project/buildSrc/allowedLicenses/BSD-3-Clause/LICENSE.txt new file mode 100644 index 0000000000000..372122bb7e2e4 --- /dev/null +++ b/fork-project/buildSrc/allowedLicenses/BSD-3-Clause/LICENSE.txt @@ -0,0 +1,25 @@ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/fork-project/buildSrc/apply/applyAndroidXComposeImplPlugin.gradle b/fork-project/buildSrc/apply/applyAndroidXComposeImplPlugin.gradle new file mode 100644 index 0000000000000..b208f64dd2a01 --- /dev/null +++ b/fork-project/buildSrc/apply/applyAndroidXComposeImplPlugin.gradle @@ -0,0 +1,9 @@ +import androidx.build.AndroidXComposeImplPlugin + +buildscript { + dependencies { + classpath(project.files("${project.ext["outDir"]}/buildSrc/private/build/libs/private.jar")) + } +} + +apply plugin: AndroidXComposeImplPlugin diff --git a/fork-project/buildSrc/apply/applyAndroidXDocsImplPlugin.gradle b/fork-project/buildSrc/apply/applyAndroidXDocsImplPlugin.gradle new file mode 100644 index 0000000000000..2258cb7b872e5 --- /dev/null +++ b/fork-project/buildSrc/apply/applyAndroidXDocsImplPlugin.gradle @@ -0,0 +1,9 @@ +import androidx.build.docs.AndroidXDocsImplPlugin + +buildscript { + dependencies { + classpath(project.files("${project.ext["outDir"]}/buildSrc/private/build/libs/private.jar")) + } +} + +apply plugin: AndroidXDocsImplPlugin diff --git a/fork-project/buildSrc/apply/applyAndroidXImplPlugin.gradle b/fork-project/buildSrc/apply/applyAndroidXImplPlugin.gradle new file mode 100644 index 0000000000000..df987e0aabc25 --- /dev/null +++ b/fork-project/buildSrc/apply/applyAndroidXImplPlugin.gradle @@ -0,0 +1,9 @@ +import androidx.build.AndroidXImplPlugin + +buildscript { + dependencies { + classpath(project.files("${project.ext["outDir"]}/buildSrc/private/build/libs/private.jar")) + } +} + +apply plugin: AndroidXImplPlugin diff --git a/fork-project/buildSrc/apply/applyAndroidXPlaygroundRootImplPlugin.gradle b/fork-project/buildSrc/apply/applyAndroidXPlaygroundRootImplPlugin.gradle new file mode 100644 index 0000000000000..006594bd66489 --- /dev/null +++ b/fork-project/buildSrc/apply/applyAndroidXPlaygroundRootImplPlugin.gradle @@ -0,0 +1,9 @@ +import androidx.build.AndroidXPlaygroundRootImplPlugin + +buildscript { + dependencies { + classpath(project.files("${project.ext["outDir"]}/buildSrc/private/build/libs/private.jar")) + } +} + +apply plugin: AndroidXPlaygroundRootImplPlugin diff --git a/fork-project/buildSrc/apply/applyAndroidXRepackageImplPlugin.gradle b/fork-project/buildSrc/apply/applyAndroidXRepackageImplPlugin.gradle new file mode 100644 index 0000000000000..820b0b5745bc3 --- /dev/null +++ b/fork-project/buildSrc/apply/applyAndroidXRepackageImplPlugin.gradle @@ -0,0 +1,9 @@ +import androidx.build.AndroidXRepackageImplPlugin + +buildscript { + dependencies { + classpath(project.files("${project.ext["outDir"]}/buildSrc/private/build/libs/private.jar")) + } +} + +apply plugin: AndroidXRepackageImplPlugin diff --git a/fork-project/buildSrc/apply/applyAndroidXRootImplPlugin.gradle b/fork-project/buildSrc/apply/applyAndroidXRootImplPlugin.gradle new file mode 100644 index 0000000000000..a9be6a22070de --- /dev/null +++ b/fork-project/buildSrc/apply/applyAndroidXRootImplPlugin.gradle @@ -0,0 +1,9 @@ +import androidx.build.AndroidXRootImplPlugin + +buildscript { + dependencies { + classpath(project.files("${project.ext["outDir"]}/buildSrc/private/build/libs/private.jar")) + } +} + +apply plugin: AndroidXRootImplPlugin diff --git a/fork-project/buildSrc/apply/applyJetBrainsAndroidXImplPlugin.gradle b/fork-project/buildSrc/apply/applyJetBrainsAndroidXImplPlugin.gradle new file mode 100644 index 0000000000000..9ec4cf2369b48 --- /dev/null +++ b/fork-project/buildSrc/apply/applyJetBrainsAndroidXImplPlugin.gradle @@ -0,0 +1,9 @@ +import org.jetbrains.androidx.build.JetBrainsAndroidXImplPlugin + +buildscript { + dependencies { + classpath(project.files("${project.ext["outDir"]}/buildSrc/private/build/libs/private.jar")) + } +} + +apply plugin: JetBrainsAndroidXImplPlugin diff --git a/fork-project/buildSrc/apply/applyJetBrainsAndroidXRootImplPlugin.gradle b/fork-project/buildSrc/apply/applyJetBrainsAndroidXRootImplPlugin.gradle new file mode 100644 index 0000000000000..29ee2e7268573 --- /dev/null +++ b/fork-project/buildSrc/apply/applyJetBrainsAndroidXRootImplPlugin.gradle @@ -0,0 +1,9 @@ +import org.jetbrains.androidx.build.JetBrainsAndroidXRootImplPlugin + +buildscript { + dependencies { + classpath(project.files("${project.ext["outDir"]}/buildSrc/private/build/libs/private.jar")) + } +} + +apply plugin: JetBrainsAndroidXRootImplPlugin diff --git a/fork-project/buildSrc/blank-proguard-rules/proguard-rules.pro b/fork-project/buildSrc/blank-proguard-rules/proguard-rules.pro new file mode 100644 index 0000000000000..3893b4cadb454 --- /dev/null +++ b/fork-project/buildSrc/blank-proguard-rules/proguard-rules.pro @@ -0,0 +1 @@ +# Intentionally empty proguard rules to indicate this library is safe to shrink diff --git a/fork-project/buildSrc/blank-res-api/public.txt b/fork-project/buildSrc/blank-res-api/public.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/fork-project/buildSrc/build.gradle b/fork-project/buildSrc/build.gradle new file mode 100644 index 0000000000000..fb8cc7041d7b1 --- /dev/null +++ b/fork-project/buildSrc/build.gradle @@ -0,0 +1,32 @@ +buildscript { + project.ext.supportRootFolder = project.projectDir.getParentFile() + apply from: "repos.gradle" + repos.addMavenRepositories(repositories) + + dependencies { + classpath(libs.kotlinGradlePlugin) + } + + configurations.classpath.resolutionStrategy { + eachDependency { details -> + if (details.requested.group == "org.jetbrains.kotlin") { + details.useVersion libs.versions.kotlin.get() + } + } + } +} + +ext.supportRootFolder = project.projectDir.getParentFile() +apply from: "repos.gradle" +apply plugin: "kotlin" + +repos.addMavenRepositories(repositories) + +project.tasks.withType(Jar).configureEach { task -> + task.reproducibleFileOrder = true + task.preserveFileTimestamps = false +} + +dependencies { + api(project("plugins")) +} diff --git a/fork-project/buildSrc/gradlew b/fork-project/buildSrc/gradlew new file mode 100644 index 0000000000000..1b6c787337ffb --- /dev/null +++ b/fork-project/buildSrc/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/fork-project/buildSrc/imports/README.md b/fork-project/buildSrc/imports/README.md new file mode 100644 index 0000000000000..7de92c8b84d35 --- /dev/null +++ b/fork-project/buildSrc/imports/README.md @@ -0,0 +1,3 @@ +This directory contains projects that just mirror the corresponding project in the main build in ../.. + +This may be useful if a project in ../.. creates a plugin that another project wants to apply diff --git a/fork-project/buildSrc/imports/baseline-profile-gradle-plugin/build.gradle b/fork-project/buildSrc/imports/baseline-profile-gradle-plugin/build.gradle new file mode 100644 index 0000000000000..ddc5fe809672c --- /dev/null +++ b/fork-project/buildSrc/imports/baseline-profile-gradle-plugin/build.gradle @@ -0,0 +1,30 @@ +apply from: "../../shared.gradle" +apply plugin: "java-gradle-plugin" + +sourceSets { + main.java.srcDirs += "${supportRootFolder}" + + "/benchmark/baseline-profile-gradle-plugin/src/main/kotlin" + main.resources.srcDirs += "${supportRootFolder}" + + "/benchmark/baseline-profile-gradle-plugin/src/main/resources" +} + +gradlePlugin { + plugins { + baselineProfileProducer { + id = "androidx.baselineprofile.producer" + implementationClass = "androidx.baselineprofile.gradle.producer.BaselineProfileProducerPlugin" + } + baselineProfileConsumer { + id = "androidx.baselineprofile.consumer" + implementationClass = "androidx.baselineprofile.gradle.consumer.BaselineProfileConsumerPlugin" + } + baselineProfileAppTarget { + id = "androidx.baselineprofile.apptarget" + implementationClass = "androidx.baselineprofile.gradle.apptarget.BaselineProfileAppTargetPlugin" + } + baselineProfileWrapper { + id = "androidx.baselineprofile" + implementationClass = "androidx.baselineprofile.gradle.wrapper.BaselineProfileWrapperPlugin" + } + } +} diff --git a/fork-project/buildSrc/imports/benchmark-darwin-plugin/build.gradle b/fork-project/buildSrc/imports/benchmark-darwin-plugin/build.gradle new file mode 100644 index 0000000000000..f8778113b4c08 --- /dev/null +++ b/fork-project/buildSrc/imports/benchmark-darwin-plugin/build.gradle @@ -0,0 +1,20 @@ +apply from: "../../shared.gradle" +apply plugin: "java-gradle-plugin" + +sourceSets { + main.java.srcDirs += "${supportRootFolder}/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin" + main.resources.srcDirs += "${supportRootFolder}/benchmark/benchmark-darwin-gradle-plugin/src/main/resources" +} + +dependencies { + implementation(libs.apacheCommonsMath) +} + +gradlePlugin { + plugins { + darwinBenchmark { + id = "androidx.benchmark.darwin" + implementationClass = "androidx.benchmark.darwin.gradle.DarwinBenchmarkPlugin" + } + } +} diff --git a/fork-project/buildSrc/imports/benchmark-gradle-plugin/build.gradle b/fork-project/buildSrc/imports/benchmark-gradle-plugin/build.gradle new file mode 100644 index 0000000000000..91ebfc363f2b9 --- /dev/null +++ b/fork-project/buildSrc/imports/benchmark-gradle-plugin/build.gradle @@ -0,0 +1,16 @@ +apply from: "../../shared.gradle" +apply plugin: "java-gradle-plugin" + +sourceSets { + main.java.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/kotlin" + main.resources.srcDirs += "${supportRootFolder}/benchmark/gradle-plugin/src/main/resources" +} + +gradlePlugin { + plugins { + benchmark { + id = "androidx.benchmark" + implementationClass = "androidx.benchmark.gradle.BenchmarkPlugin" + } + } +} diff --git a/fork-project/buildSrc/imports/binary-compatibility-validator/build.gradle b/fork-project/buildSrc/imports/binary-compatibility-validator/build.gradle new file mode 100644 index 0000000000000..552ec44f8991d --- /dev/null +++ b/fork-project/buildSrc/imports/binary-compatibility-validator/build.gradle @@ -0,0 +1,36 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply from: "../../shared.gradle" + +// TODO(b/410631668): remove when "kotlin-compiler" is no longer added to "friendPaths" +// Workaround for Windows to solve +// "this and base files have different roots: C:\Users\User\.gradle\caches\modules-2\...\kotlin-compiler-2.2.10.jar and D:\compose-multiplatform-core\out\buildSrc\imports\binary-compatibility-validator\build" +// +// This happens because "friendPaths" is set and it doesn't support different roots (C: and D:) +// kotlin-compiler was added to "friendsPath" in +// https://android-review.googlesource.com/c/platform/frameworks/support/+/3636427 +// +// This moves the build directory to the Gradle cache directory for this module, +// which is an anti-pattern but solves the issue +if (System.properties['os.name']?.toString()?.toLowerCase()?.contains('windows') == true) { + layout.buildDirectory = file("${gradle.gradleUserHomeDir}/compose-multipltform-core-build/buildSrc-imports-binary-compatibility-validator") +} + +sourceSets { + main.java.srcDirs += "${supportRootFolder}/binarycompatibilityvalidator/" + + "binarycompatibilityvalidator/src/jvmMain/kotlin" +} diff --git a/fork-project/buildSrc/imports/glance-layout-generator/build.gradle b/fork-project/buildSrc/imports/glance-layout-generator/build.gradle new file mode 100644 index 0000000000000..71f1bec8e2d63 --- /dev/null +++ b/fork-project/buildSrc/imports/glance-layout-generator/build.gradle @@ -0,0 +1,6 @@ +apply from: "../../shared.gradle" + +sourceSets { + main.java.srcDirs += "${supportRootFolder}/glance/glance-appwidget/glance-layout-generator/" + + "src/main/kotlin" +} diff --git a/fork-project/buildSrc/imports/inspection-gradle-plugin/build.gradle b/fork-project/buildSrc/imports/inspection-gradle-plugin/build.gradle new file mode 100644 index 0000000000000..586cf70654371 --- /dev/null +++ b/fork-project/buildSrc/imports/inspection-gradle-plugin/build.gradle @@ -0,0 +1,17 @@ +apply from: "../../shared.gradle" +apply plugin: "java-gradle-plugin" + +sourceSets { + main.java.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main/kotlin" + main.resources.srcDirs += "${supportRootFolder}/inspection/inspection-gradle-plugin/src/main" + + "/resources" +} + +gradlePlugin { + plugins { + inspection { + id = "androidx.inspection" + implementationClass = "androidx.inspection.gradle.InspectionPlugin" + } + } +} diff --git a/fork-project/buildSrc/imports/room-gradle-plugin/build.gradle b/fork-project/buildSrc/imports/room-gradle-plugin/build.gradle new file mode 100644 index 0000000000000..f940ed8c0cc64 --- /dev/null +++ b/fork-project/buildSrc/imports/room-gradle-plugin/build.gradle @@ -0,0 +1,17 @@ +apply from: "../../shared.gradle" +apply plugin: "java-gradle-plugin" + +sourceSets { + main.java.srcDirs += "${supportRootFolder}/room3/room3-gradle-plugin/src/main/java" + main.resources.srcDirs += "${supportRootFolder}/room3/room3-gradle-plugin/src/main" + + "/resources" +} + +gradlePlugin { + plugins { + room { + id = "androidx.room3" + implementationClass = "androidx.room3.gradle.RoomGradlePlugin" + } + } +} diff --git a/fork-project/buildSrc/imports/stableaidl-gradle-plugin/build.gradle b/fork-project/buildSrc/imports/stableaidl-gradle-plugin/build.gradle new file mode 100644 index 0000000000000..cec331b00fc9d --- /dev/null +++ b/fork-project/buildSrc/imports/stableaidl-gradle-plugin/build.gradle @@ -0,0 +1,15 @@ +apply from: "../../shared.gradle" +apply plugin: "java-gradle-plugin" + +sourceSets { + main.java.srcDirs += "${supportRootFolder}/stableaidl/stableaidl-gradle-plugin/src/main/java" +} + +gradlePlugin { + plugins { + stableaidl { + id = "androidx.stableaidl" + implementationClass = "androidx.stableaidl.StableAidlPlugin" + } + } +} diff --git a/fork-project/buildSrc/jetpad-integration/build.gradle b/fork-project/buildSrc/jetpad-integration/build.gradle new file mode 100644 index 0000000000000..72425861c9555 --- /dev/null +++ b/fork-project/buildSrc/jetpad-integration/build.gradle @@ -0,0 +1,10 @@ +apply plugin:"java" + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} +project.tasks.withType(Jar).configureEach { task -> + task.reproducibleFileOrder = true + task.preserveFileTimestamps = false +} diff --git a/fork-project/buildSrc/jetpad-integration/src/main/java/androidx/build/jetpad/LibraryBuildInfoFile.java b/fork-project/buildSrc/jetpad-integration/src/main/java/androidx/build/jetpad/LibraryBuildInfoFile.java new file mode 100644 index 0000000000000..719c3d3d63d8d --- /dev/null +++ b/fork-project/buildSrc/jetpad-integration/src/main/java/androidx/build/jetpad/LibraryBuildInfoFile.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.jetpad; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Objects; +import java.util.Set; + +/** + * Object outlining the format of a library's build info file. + * This object will be serialized to json. + * This file should match the corresponding class in Jetpad because + * this object will be serialized to json and the result will be parsed by Jetpad. + * DO NOT TOUCH. + * + * @property groupId library maven group Id + * @property artifactId library maven artifact Id + * @property version library maven version + * @property path local project directory path used for development, rooted at framework/support + * @property sha the sha of the latest commit to modify the library (aka a commit that + * touches a file within [path]) + * @property groupIdRequiresSameVersion boolean that determines if all libraries with [groupId] + * have the same version + * @property dependencies a list of dependencies on other androidx libraries + * @property checks arraylist of [Check]s that is used by Jetpad + */ +public final class LibraryBuildInfoFile { + public String groupId; + public String artifactId; + public String version; + public String kotlinVersion; + public String path; + public String sha; + public String projectZipPath; + public Boolean groupIdRequiresSameVersion; + public ArrayList dependencies; + public ArrayList allDependencies; + public ArrayList dependencyConstraints; + public Boolean shouldPublishDocs; + public Boolean isKmp; + public String target; + public ArrayList checks; + public Set kmpChildren; + public Set testModuleNames; + public Set gradlePluginIds; + + /** + * @property isTipOfTree boolean that specifies whether the dependency is tip-of-tree + */ + public static final class Dependency implements Serializable { + public String groupId; + public String artifactId; + public String version; + public boolean isTipOfTree; + public static final long serialVersionUID = 12345L; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Dependency that = (Dependency) o; + return isTipOfTree == that.isTipOfTree && groupId.equals(that.groupId) + && artifactId.equals( + that.artifactId) && version.equals(that.version); + } + + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId, version, isTipOfTree); + } + } + + public static final class Check { + public String name; + public boolean passing; + } +} diff --git a/fork-project/buildSrc/karmaconfig/karma.conf.js b/fork-project/buildSrc/karmaconfig/karma.conf.js new file mode 100644 index 0000000000000..ba037048f39c5 --- /dev/null +++ b/fork-project/buildSrc/karmaconfig/karma.conf.js @@ -0,0 +1,30 @@ +// Set a fairly long test timeout because some tests in collection +// (specifically insertManyRemoveMany) occasionally take 20+ seconds to complete. +var testTimeoutInMs = 3000 * 30 +// disconnect timeout should be longer than test timeout so we don't disconnect before the timeout +// is reported. +var browserDisconnectTimeoutInMs = testTimeoutInMs + 5000 +config.set({ + // https://karma-runner.github.io/6.4/config/configuration-file.html + browserDisconnectTimeout: browserDisconnectTimeoutInMs, + processKillTimeout: testTimeoutInMs, + concurrency: 1, + client: { + mocha: { + timeout: testTimeoutInMs + } + } +}); + +// Add 10 second delay exit to ensure log flushing. This is needed for Kotlin to avoid flakiness when +// marking a test as complete. See (b/382336155) +// Remove when https://youtrack.jetbrains.com/issue/KT-73911/ is resolved. +(function() { + const originalExit = process.exit; + process.exit = function(code) { + console.log('Delaying exit for logs...'); + setTimeout(() => { + originalExit(code); + }, 10000); + }; +})(); diff --git a/fork-project/buildSrc/kotlin-dsl-dependency.gradle b/fork-project/buildSrc/kotlin-dsl-dependency.gradle new file mode 100644 index 0000000000000..ae4194c94977d --- /dev/null +++ b/fork-project/buildSrc/kotlin-dsl-dependency.gradle @@ -0,0 +1,35 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def findGradleKotlinDsl() { + /* + * TODO(137044144): After we convert this file to Kotlin (build.gradle.kts), we can just + * directly call the getGradleKotlinDsl() method. + * We're not doing that yet though because Gradle takes more time to process .kts files (at the + * time of writing, adding a .kts file adds roughly 10 addition seconds to build startup time). + * + * getGradleVersion() is in a format of X.Y.Z-rc-1 / X.Y.Z. Kotlin dsl jar always drops the + * "-rc-1" suffix of the version, thus we need additional substring logic. + */ + def dashIndex = project.gradle.getGradleVersion().indexOf("-") + def kotlinDslVersion = dashIndex == -1 ? project.gradle.getGradleVersion() + : project.gradle.getGradleVersion().substring(0, dashIndex) + def kotlinDsl = "" + project.gradle.getGradleHomeDir() + "/lib/gradle-kotlin-dsl-" + + kotlinDslVersion + ".jar" + return project.files(kotlinDsl) +} + +ext.findGradleKotlinDsl = this.&findGradleKotlinDsl diff --git a/fork-project/buildSrc/lint/lint.xml b/fork-project/buildSrc/lint/lint.xml new file mode 100644 index 0000000000000..a9c2cd4633644 --- /dev/null +++ b/fork-project/buildSrc/lint/lint.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fork-project/buildSrc/lint/lint_samples.xml b/fork-project/buildSrc/lint/lint_samples.xml new file mode 100644 index 0000000000000..176a88eb1d842 --- /dev/null +++ b/fork-project/buildSrc/lint/lint_samples.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/fork-project/buildSrc/ndk.gradle b/fork-project/buildSrc/ndk.gradle new file mode 100644 index 0000000000000..ee863e51da625 --- /dev/null +++ b/fork-project/buildSrc/ndk.gradle @@ -0,0 +1,3 @@ +android { + ndkVersion = "27.0.12077973" +} diff --git a/fork-project/buildSrc/plugins/README.md b/fork-project/buildSrc/plugins/README.md new file mode 100644 index 0000000000000..363e1b4da1c93 --- /dev/null +++ b/fork-project/buildSrc/plugins/README.md @@ -0,0 +1,5 @@ +This is the :buildSrc:plugins project + +It contains plugins to be applied by various other projects in this repository + +The plugins in this project do not get published to remote repositories diff --git a/fork-project/buildSrc/plugins/build.gradle b/fork-project/buildSrc/plugins/build.gradle new file mode 100644 index 0000000000000..ec15d575df5c1 --- /dev/null +++ b/fork-project/buildSrc/plugins/build.gradle @@ -0,0 +1,20 @@ +apply from: "../shared.gradle" + +dependencies { + implementation(project(":public")) + api(project(":imports:baseline-profile-gradle-plugin")) + api(project(":imports:benchmark-darwin-plugin")) + api(project(":imports:benchmark-gradle-plugin")) + api(project(":imports:binary-compatibility-validator")) + api(project(":imports:glance-layout-generator")) + api(project(":imports:inspection-gradle-plugin")) + api(project(":imports:room-gradle-plugin")) + api(project(":imports:stableaidl-gradle-plugin")) +} + + +// The artifacts built by this project require at runtime the artifacts from `:buildSrc:private`. +// However, we don't want `:buildSrc:private` artifacts to be on their runtime classpath, because +// that means that any changes to those artifacts can invalidate task up-to-datedness +// (see ../README.md) +tasks["jar"].dependsOn(":private:build") diff --git a/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXComposePlugin.kt b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXComposePlugin.kt new file mode 100644 index 0000000000000..361dc3e6aa678 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXComposePlugin.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** Plugin to apply common configuration for Compose projects. */ +class AndroidXComposePlugin : Plugin { + override fun apply(project: Project) { + val supportRoot = project.getSupportRootFolder() + project.apply( + mapOf( + "from" to "$supportRoot/buildSrc/apply/applyAndroidXComposeImplPlugin.gradle" + ) + ) + } +} diff --git a/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt new file mode 100644 index 0000000000000..622ea2ae9d918 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * This plugin is used in Playground projects and adds functionality like resolving to snapshot + * artifacts instead of projects or allowing access to public maven repositories. + * + * The actual implementation is in AndroidXRootImplPlugin. This extracts this logic out of the + * classpath so that individual tasks can't access this logic so Gradle can know that changes to + * this logic doesn't need to automatically invalidate every task + */ +@Suppress("unused") // used in Playground Projects +class AndroidXPlaygroundRootPlugin : Plugin { + override fun apply(project: Project) { + val supportRoot = project.getSupportRootFolder() + project.apply( + mapOf( + "from" to "$supportRoot/buildSrc/apply/applyAndroidXPlaygroundRootImplPlugin.gradle" + ) + ) + } +} diff --git a/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPlugin.kt b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPlugin.kt new file mode 100644 index 0000000000000..9314cf0f966da --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPlugin.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * A plugin which enables all of the Gradle customizations for AndroidX. This plugin reacts to other + * plugins being added and adds required and optional functionality. + * + * The actual implementation is in AndroidXImplPlugin. This extracts this logic out of the classpath + * so that individual tasks can't access this logic so Gradle can know that changes to this logic + * doesn't need to automatically invalidate every task + */ +class AndroidXPlugin : Plugin { + override fun apply(project: Project) { + val supportRoot = project.getSupportRootFolder() + project.apply( + mapOf( + "from" to "$supportRoot/buildSrc/apply/applyAndroidXImplPlugin.gradle" + ) + ) + } + + companion object { + /** @return `true` if running in a Playground (Github) setup, `false` otherwise. */ + @JvmStatic + fun isPlayground(project: Project): Boolean { + return ProjectLayoutType.isPlayground(project) + } + } +} diff --git a/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXRepackagePlugin.kt b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXRepackagePlugin.kt new file mode 100644 index 0000000000000..402919aebeade --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXRepackagePlugin.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * This plugin is responsible for repackaging libraries. + * + * The actual implementation is in AndroidXRepackageImplPlugin. This extracts this logic out of the + * classpath so that individual tasks can't access this logic so Gradle can know that changes to + * this logic doesn't need to automatically invalidate every task + */ +abstract class AndroidXRepackagePlugin : Plugin { + override fun apply(project: Project) { + val supportRoot = project.getSupportRootFolder() + project.apply( + mapOf( + "from" to "$supportRoot/buildSrc/apply/applyAndroidXRepackageImplPlugin.gradle" + ) + ) + } +} diff --git a/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt new file mode 100644 index 0000000000000..ba7f3509aabaf --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * This plugin needs to be applied to the root of an AndroidX build + * + * The actual implementation is in AndroidXRootImplPlugin. This extracts this logic out of the + * classpath so that individual tasks can't access this logic so Gradle can know that changes to + * this logic doesn't need to automatically invalidate every task + */ +abstract class AndroidXRootPlugin : Plugin { + override fun apply(project: Project) { + val supportRoot = project.getSupportRootFolder() + project.apply( + mapOf( + "from" to "$supportRoot/buildSrc/apply/applyAndroidXRootImplPlugin.gradle" + ) + ) + } +} diff --git a/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt new file mode 100644 index 0000000000000..fdb3768896e0f --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/kotlin/androidx/build/docs/AndroidXDocsPlugin.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.docs + +import androidx.build.getSupportRootFolder +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * Plugin that allows to build documentation for a given set of prebuilt and tip of tree projects. + * + * The actual implementation is in AndroidXDocsImplPlugin. This extracts this logic out of the + * classpath so that individual tasks can't access this logic so Gradle can know that changes to + * this logic doesn't need to automatically invalidate every task + */ +class AndroidXDocsPlugin : Plugin { + override fun apply(project: Project) { + val supportRoot = project.getSupportRootFolder() + project.apply( + mapOf( + "from" to "$supportRoot/buildSrc/apply/applyAndroidXDocsImplPlugin.gradle" + ) + ) + } +} diff --git a/fork-project/buildSrc/plugins/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXPlugin.kt b/fork-project/buildSrc/plugins/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXPlugin.kt new file mode 100644 index 0000000000000..a5c38d6f8a183 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXPlugin.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.androidx.build + +import androidx.build.getSupportRootFolder +import org.gradle.api.Plugin +import org.gradle.api.Project + +class JetBrainsAndroidXPlugin : Plugin { + override fun apply(project: Project) { + val supportRoot = project.getSupportRootFolder() + project.apply( + mapOf( + "from" to "$supportRoot/buildSrc/apply/applyJetBrainsAndroidXImplPlugin.gradle" + ) + ) + } +} \ No newline at end of file diff --git a/fork-project/buildSrc/plugins/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXRootPlugin.kt b/fork-project/buildSrc/plugins/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXRootPlugin.kt new file mode 100644 index 0000000000000..ed8c7a944fb42 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/kotlin/org/jetbrains/androidx/build/JetBrainsAndroidXRootPlugin.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.androidx.build + +import androidx.build.getSupportRootFolder +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * This plugin needs to be applied to the root of an AndroidX build + */ +abstract class JetBrainsAndroidXRootPlugin : Plugin { + override fun apply(project: Project) { + val supportRoot = project.getSupportRootFolder() + project.apply( + mapOf( + "from" to "$supportRoot/buildSrc/apply/applyJetBrainsAndroidXRootImplPlugin.gradle" + ) + ) + } +} diff --git a/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXComposePlugin.properties b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXComposePlugin.properties new file mode 100644 index 0000000000000..8752241f9c525 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXComposePlugin.properties @@ -0,0 +1,17 @@ +# +# Copyright 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=androidx.build.AndroidXComposePlugin \ No newline at end of file diff --git a/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXDocsPlugin.properties b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXDocsPlugin.properties new file mode 100644 index 0000000000000..49374aa1d3d05 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXDocsPlugin.properties @@ -0,0 +1,17 @@ +# +# Copyright 2020 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=androidx.build.docs.AndroidXDocsPlugin \ No newline at end of file diff --git a/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPlaygroundRootPlugin.properties b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPlaygroundRootPlugin.properties new file mode 100644 index 0000000000000..85b7cddac8c81 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPlaygroundRootPlugin.properties @@ -0,0 +1,17 @@ +# +# Copyright 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=androidx.build.AndroidXPlaygroundRootPlugin \ No newline at end of file diff --git a/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPlugin.properties b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPlugin.properties new file mode 100644 index 0000000000000..471d300c44d46 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPlugin.properties @@ -0,0 +1,17 @@ +# +# Copyright 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=androidx.build.AndroidXPlugin \ No newline at end of file diff --git a/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXRepackagePlugin.properties b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXRepackagePlugin.properties new file mode 100644 index 0000000000000..bbed5f87ee328 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXRepackagePlugin.properties @@ -0,0 +1,17 @@ +# +# Copyright 2024 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=androidx.build.AndroidXRepackagePlugin \ No newline at end of file diff --git a/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXRootPlugin.properties b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXRootPlugin.properties new file mode 100644 index 0000000000000..df0100e343dcb --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXRootPlugin.properties @@ -0,0 +1,17 @@ +# +# Copyright 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=androidx.build.AndroidXRootPlugin \ No newline at end of file diff --git a/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/JetBrainsAndroidXPlugin.properties b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/JetBrainsAndroidXPlugin.properties new file mode 100644 index 0000000000000..c1095b8210f22 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/JetBrainsAndroidXPlugin.properties @@ -0,0 +1,17 @@ +# +# Copyright 2024 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=org.jetbrains.androidx.build.JetBrainsAndroidXPlugin \ No newline at end of file diff --git a/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/JetBrainsAndroidXRootPlugin.properties b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/JetBrainsAndroidXRootPlugin.properties new file mode 100644 index 0000000000000..73982f38e4de6 --- /dev/null +++ b/fork-project/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/JetBrainsAndroidXRootPlugin.properties @@ -0,0 +1,17 @@ +# +# Copyright 2025 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +implementation-class=org.jetbrains.androidx.build.JetBrainsAndroidXRootPlugin \ No newline at end of file diff --git a/fork-project/buildSrc/private/README.md b/fork-project/buildSrc/private/README.md new file mode 100644 index 0000000000000..3f7a53aaca839 --- /dev/null +++ b/fork-project/buildSrc/private/README.md @@ -0,0 +1,7 @@ +This is the :buildSrc:private project + +It contains code that is used to configure other projects in this repository but that does not need to be added to the classpaths of the build scripts of those projects. + +This means that if code in this project is changed, it should not necessarily modify the classpath of those projects and should not automatically invalidate the up-to-datedness of tasks applied in those projects. + +See b/140265324 for more information diff --git a/fork-project/buildSrc/private/build.gradle b/fork-project/buildSrc/private/build.gradle new file mode 100644 index 0000000000000..7fa22d1ee0a24 --- /dev/null +++ b/fork-project/buildSrc/private/build.gradle @@ -0,0 +1,12 @@ +apply from: "../shared.gradle" +apply plugin: "java-gradle-plugin" + +dependencies { + implementation(project(":public")) + implementation(project(":imports:benchmark-gradle-plugin")) + implementation(project(":imports:inspection-gradle-plugin")) + implementation(project(":imports:stableaidl-gradle-plugin")) + implementation(project(":imports:binary-compatibility-validator")) +} + + diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt new file mode 100644 index 0000000000000..1eaa0b08c1814 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import com.android.build.api.dsl.Lint +import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension +import com.android.build.api.variant.LintLifecycleExtension +import com.android.build.gradle.AppPlugin +import com.android.build.gradle.LibraryPlugin +import com.android.build.gradle.api.KotlinMultiplatformAndroidPlugin +import java.io.File +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.type.ArtifactTypeDefinition +import org.gradle.api.attributes.Attribute +import org.gradle.api.file.FileCollection +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.plugin.CompilerPluginConfig +import org.jetbrains.kotlin.gradle.plugin.KotlinBaseApiPlugin +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper +import org.jetbrains.kotlin.gradle.plugin.SubpluginOption +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinNativeCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile + +/** Plugin to apply common configuration for Compose projects. */ +class AndroidXComposeImplPlugin : Plugin { + override fun apply(project: Project) { + project.plugins.configureEach { plugin -> + when (plugin) { + is AppPlugin, + is LibraryPlugin -> { + project.extensions + .findByType(LintLifecycleExtension::class.java)!! + .finalizeDsl { project.configureAndroidCommonOptions(it) } + } + is KotlinMultiplatformAndroidPlugin -> { + project.extensions + .getByType() + .finalizeDsl { project.configureAndroidCommonOptions(it.lint) } + } + is KotlinBasePluginWrapper, + is KotlinBaseApiPlugin -> { + configureComposeCompilerPlugin(project) + } + } + } + + // JetBrains fork: allow native experimental features globally + // TODO: Move to module config before upstreaming + project.tasks.withType(KotlinNativeCompile::class.java).configureEach { + it.compilerOptions.freeCompilerArgs.addAll( + "-opt-in=kotlinx.cinterop.ExperimentalForeignApi", + "-opt-in=kotlin.experimental.ExperimentalNativeApi" + ) + } + } + + companion object { + private fun Project.configureAndroidCommonOptions(lint: Lint) { + val isPublished = androidXExtension.shouldPublish.get() + val type = androidXExtension.type.get() + + lint.apply { + // These lint checks are normally a warning (or lower), but we ignore (in + // AndroidX) + // warnings in Lint, so we make it an error here so it will fail the build. + // Note that this causes 'UnknownIssueId' lint warnings in the build log when + // Lint tries to apply this rule to modules that do not have this lint check, so + // we disable that check too + disable.add("UnknownIssueId") + error.addAll(ComposeLintWarningIdsToTreatAsErrors) + + // Paths we want to disable ListIteratorChecks for + val ignoreListIteratorFilter = + listOf( + // These are not runtime libraries and so Iterator allocation is not + // relevant. + "compose:ui:ui-test", + "compose:ui:ui-tooling", + "compose:ui:ui-inspection", + // Navigation libraries are not in performance critical paths, so we can + // ignore them. + "navigation:navigation-compose", + "wear:compose:compose-navigation", + ) + + // Disable ListIterator if we are not in a matching path, or we are in an + // unpublished project + if (ignoreListIteratorFilter.any { path.contains(it) } || !isPublished) { + disable.add("ListIterator") + } + + // b/333784604 Disable ConfigurationScreenWidthHeight for wear libraries, it + // does not apply to wear + if (path.startsWith(":wear:")) { + disable.add("ConfigurationScreenWidthHeight") + } + + // These checks are not required for samples projects. + if (type == SoftwareType.SAMPLES) { + disable.add("ListIterator") + disable.add("PrimitiveInCollection") + } + + // Disable lambda creation in subcompose check in projects where we're less + // concerned about performance. + if ( + type in + setOf( + SoftwareType.TEST_APPLICATION, + SoftwareType.PUBLISHED_KOTLIN_ONLY_TEST_LIBRARY, + SoftwareType.PUBLISHED_TEST_LIBRARY, + SoftwareType.SAMPLES, + SoftwareType.UNSET, + ) + ) { + disable.add("ComposableLambdaInMeasurePolicy") + } + } + + if (!allowMissingLintProject()) { + // TODO: figure out how to apply this to multiplatform modules + dependencies.add( + "lintChecks", + project.dependencies.project( + mapOf( + "path" to ":compose:lint:internal-lint-checks", + // TODO(b/206617878) remove this shadow configuration + "configuration" to "shadow", + ) + ), + ) + } + } + } +} + +private fun configureComposeCompilerPlugin(project: Project) { + project.afterEvaluate { + // Add Compose compiler plugin to kotlinPlugin configuration, making sure it works + // for Playground builds as well + val isPlayground = ProjectLayoutType.isPlayground(project) + val compilerPluginVersion = + project.getVersionByName(if (isPlayground) "kotlin" else "composeCompilerPlugin") + // Create configuration that we'll use to load Compose compiler plugin + val configuration = + project.configurations.detachedConfiguration( + project.dependencies.create( + "org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:$compilerPluginVersion" + ) + ) + + if ( + compilerPluginVersion.endsWith("-SNAPSHOT") && + !isPlayground && + // ksp is also a compiler plugin, updating Kotlin for it will likely break the build + !project.plugins.hasPlugin("com.google.devtools.ksp") + ) { + // use exact project path instead of subprojects.find, it is faster + val compilerProject = project.rootProject.resolveProject(":compose") + val compilerMavenDirectory = + File(compilerProject.projectDir, "compiler/compose-compiler-snapshot-repository") + project.repositories.maven { it.url = compilerMavenDirectory.toURI() } + project.configurations.configureEach { + it.resolutionStrategy.eachDependency { dep -> + val requested = dep.requested + if ( + requested.group == "org.jetbrains.kotlin" && + (requested.name == "kotlin-compiler-embeddable" || + requested.name == "kotlin-compose-compiler-plugin-embeddable") + ) { + dep.useVersion(compilerPluginVersion) + } + } + } + } + + val kotlinPlugin = + configuration.incoming + .artifactView { view -> + view.attributes { attributes -> + attributes.attribute( + Attribute.of("artifactType", String::class.java), + ArtifactTypeDefinition.JAR_TYPE, + ) + } + } + .files + + project.tasks.withType(KotlinCompilationTask::class.java).configureEach { compile -> + compile.applyPlugin(kotlinPlugin) + + val isAndroidOrJvm = compile is KotlinJvmCompile + + compile.addPluginOption(ComposeCompileOptions.SourceOption, isAndroidOrJvm.toString()) + compile.addPluginOption( + ComposeCompileOptions.TraceMarkersOption, + isAndroidOrJvm.toString(), + ) + } + } +} + +private fun KotlinCompilationTask<*>.applyPlugin(plugins: FileCollection) = + when (this) { + is AbstractKotlinCompile<*> -> pluginClasspath.from(plugins) + is AbstractKotlinNativeCompile<*, *> -> compilerPluginClasspath = plugins + else -> throw IllegalStateException("Unsupported Kotlin compilation task type") + } + +private fun KotlinCompilationTask<*>.addPluginArgument(pluginId: String, option: SubpluginOption) = + when (this) { + is AbstractKotlinCompile<*> -> + pluginOptions.add(CompilerPluginConfig().apply { addPluginArgument(pluginId, option) }) + is AbstractKotlinNativeCompile<*, *> -> + compilerPluginOptions.addPluginArgument(pluginId, option) + else -> throw IllegalStateException("Unsupported Kotlin compilation task type") + } + +private fun KotlinCompilationTask<*>.addPluginOption( + composeCompileOptions: ComposeCompileOptions, + value: String, +) = + addPluginArgument( + pluginId = composeCompileOptions.pluginId, + option = SubpluginOption(composeCompileOptions.key, value), + ) + +private fun KotlinCompilationTask<*>.enableFeatureFlag(featureFlag: ComposeFeatureFlag) { + addPluginOption(ComposeCompileOptions.FeatureFlagOption, featureFlag.featureName) +} + +private const val ComposePluginId = "androidx.compose.compiler.plugins.kotlin" + +private enum class ComposeCompileOptions(val pluginId: String, val key: String) { + SourceOption(ComposePluginId, "sourceInformation"), + TraceMarkersOption(ComposePluginId, "traceMarkersEnabled"), + StrongSkipping(ComposePluginId, "strongSkipping"), + NonSkippingGroupOptimization(ComposePluginId, "nonSkippingGroupOptimization"), + FeatureFlagOption(ComposePluginId, "featureFlag"), +} + +private enum class ComposeFeatureFlag(val featureName: String) { + StrongSkipping("StrongSkipping"), + OptimizeNonSkippingGroups("OptimizeNonSkippingGroups"), + PausableComposition("PausableComposition"), +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeLintIssues.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeLintIssues.kt new file mode 100644 index 0000000000000..508a76365335f --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeLintIssues.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +/** + * These lint checks are normally a warning (or lower), but in AndroidX we ignore warnings in Lint. + * We want these errors to be reported, so they'll be promoted from a warning to an error in modules + * that use the [AndroidXComposeImplPlugin]. + */ +internal val ComposeLintWarningIdsToTreatAsErrors = + listOf( + "ComposableNaming", + "ComposableLambdaParameterNaming", + "ComposableLambdaParameterPosition", + "CompositionLocalNaming", + "ComposableModifierFactory", + "AutoboxingStateCreation", + "AutoboxingStateValueProperty", + "InvalidColorHexValue", + "MissingColorAlphaChannel", + "ModifierFactoryReturnType", + "ModifierFactoryExtensionFunction", + "ModifierNodeInspectableProperties", + "ModifierParameter", + "MutableCollectionMutableState", + "OpaqueUnitKey", + "UnnecessaryComposedModifier", + "FrequentlyChangedStateReadInComposition", + "FrequentlyChangingValue", + "ReturnFromAwaitPointerEventScope", + "UseOfNonLambdaOffsetOverload", + "MultipleAwaitPointerEventScopes", + "LocalContextResourcesRead", + "ConfigurationScreenWidthHeight", + ) diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt new file mode 100644 index 0000000000000..cfa8daef83720 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.dependencyTracker.AffectedModuleDetector +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.provider.Provider + +/** + * Whether to enable constraints for projects in same-version groups + * + * This is default true. + */ +const val ADD_GROUP_CONSTRAINTS = "androidx.constraints" + +/** Setting this property to false makes test tasks not display detailed output to stdout. */ +const val DISPLAY_TEST_OUTPUT = "androidx.displayTestOutput" + +/** Setting this property changes "url" property in publishing maven artifact metadata */ +const val ALTERNATIVE_PROJECT_URL = "androidx.alternativeProjectUrl" + +/** Validate the project structure against Jetpack guidelines */ +const val VALIDATE_PROJECT_STRUCTURE = "androidx.validateProjectStructure" + +/** Returns whether the project should generate documentation. */ +const val ENABLE_DOCUMENTATION = "androidx.enableDocumentation" + +/** Setting this property puts a summary of the relevant failure messages into standard error */ +const val SUMMARIZE_STANDARD_ERROR = "androidx.summarizeStderr" + +/** + * Setting this property indicates that a build is being performed to check for forward + * compatibility. + */ +const val USE_MAX_DEP_VERSIONS = "androidx.useMaxDepVersions" + +/** Setting this property enables writing versioned API files */ +const val WRITE_VERSIONED_API_FILES = "androidx.writeVersionedApiFiles" + +/** + * Build id used to pull SNAPSHOT versions to substitute project dependencies in Playground projects + */ +const val PLAYGROUND_SNAPSHOT_BUILD_ID = "androidx.playground.snapshotBuildId" + +/** Build Id used to pull SNAPSHOT version of Metalava for Playground projects */ +const val PLAYGROUND_METALAVA_BUILD_ID = "androidx.playground.metalavaBuildId" + +/** Specifies to prepend the current time to each Gradle log message */ +const val PRINT_TIMESTAMPS = "androidx.printTimestamps" + +/** + * Filepath to the java agent of YourKit for profiling If this value is set, profiling via YourKit + * will automatically be enabled + */ +const val PROFILE_YOURKIT_AGENT_PATH = "androidx.profile.yourkitAgentPath" + +/** + * Specifies to validate that the build doesn't generate any unrecognized messages This prevents + * developers from inadvertently adding new warnings to the build output + */ +const val VALIDATE_NO_UNRECOGNIZED_MESSAGES = "androidx.validateNoUnrecognizedMessages" + +/** + * Specifies to run the build twice and validate that the second build doesn't run more tasks than + * expected. + */ +const val VERIFY_UP_TO_DATE = "androidx.verifyUpToDate" + +/** + * If true, we are building in GitHub and should enable build features related to KMP. If false, we + * are in AOSP, where not all KMP features are enabled. + */ +const val KMP_GITHUB_BUILD = "androidx.github.build" + +/** Specifies to give as much memory to Gradle as in a typical CI run */ +const val HIGH_MEMORY = "androidx.highMemory" + +/** Negates the HIGH_MEMORY flag */ +const val LOW_MEMORY = "androidx.lowMemory" + +/** + * If true, don't require lint-checks project to exist. This should only be set in integration + * tests, to allow them to save time by not configuring extra projects. + */ +const val ALLOW_MISSING_LINT_CHECKS_PROJECT = "androidx.allow.missing.lint" + +/** + * If set to a uri, this is the location that will be used to download `xcodegen` when running + * Darwin benchmarks. + */ +const val XCODEGEN_DOWNLOAD_URI = "androidx.benchmark.darwin.xcodeGenDownloadUri" + +/** If true, yarn dependencies are fetched from an offline mirror */ +const val YARN_OFFLINE_MODE = "androidx.yarnOfflineMode" + +/** Defined by AndroidX Benchmark Plugin, may be used for local experiments with compilation */ +const val FORCE_BENCHMARK_AOT_COMPILATION = "androidx.benchmark.forceaotcompilation" + +val ALL_ANDROIDX_PROPERTIES = + setOf( + ADD_GROUP_CONSTRAINTS, + ALTERNATIVE_PROJECT_URL, + VALIDATE_PROJECT_STRUCTURE, + DISPLAY_TEST_OUTPUT, + ENABLE_DOCUMENTATION, + HIGH_MEMORY, + LOW_MEMORY, + STUDIO_TYPE, + SUMMARIZE_STANDARD_ERROR, + USE_MAX_DEP_VERSIONS, + VALIDATE_NO_UNRECOGNIZED_MESSAGES, + VERIFY_UP_TO_DATE, + WRITE_VERSIONED_API_FILES, + AffectedModuleDetector.ENABLE_ARG, + AffectedModuleDetector.BASE_COMMIT_ARG, + PLAYGROUND_SNAPSHOT_BUILD_ID, + PLAYGROUND_METALAVA_BUILD_ID, + PRINT_TIMESTAMPS, + PROFILE_YOURKIT_AGENT_PATH, + KMP_GITHUB_BUILD, + ENABLED_KMP_TARGET_PLATFORMS, + ALLOW_MISSING_LINT_CHECKS_PROJECT, + XCODEGEN_DOWNLOAD_URI, + FilteredAnchorTask.PROP_TASK_NAME, + FilteredAnchorTask.PROP_PATH_PREFIX, + YARN_OFFLINE_MODE, + FORCE_BENCHMARK_AOT_COMPILATION, + ) + AndroidConfigImpl.GRADLE_PROPERTIES + +/** + * Whether to enable constraints for projects in same-version groups See the property definition for + * more details + */ +fun Project.shouldAddGroupConstraints(): Provider = + project.providers.gradleProperty(ADD_GROUP_CONSTRAINTS).map { s -> s.toBoolean() }.orElse(true) + +/** + * Returns alternative project url that will be used as "url" property in publishing maven artifact + * metadata. + * + * Returns null if there is no alternative project url. + */ +fun Project.getAlternativeProjectUrl(): String? = + project.providers.gradleProperty(ALTERNATIVE_PROJECT_URL).getOrNull() + +/** Validate the project structure against Jetpack guidelines */ +fun Project.isValidateProjectStructureEnabled(): Boolean = + findBooleanProperty(VALIDATE_PROJECT_STRUCTURE) ?: true + +/** + * Validates that all properties passed by the user of the form "-Pandroidx.*" are not misspelled + */ +fun Project.validateAllAndroidxArgumentsAreRecognized() { + for (propertyName in project.properties.keys) { + if (propertyName.startsWith("androidx")) { + if (!ALL_ANDROIDX_PROPERTIES.contains(propertyName)) { + val message = + "Unrecognized Androidx property '$propertyName'.\n" + + "\n" + + "Is this a misspelling? All recognized Androidx properties:\n" + + ALL_ANDROIDX_PROPERTIES.joinToString("\n") + + "\n" + + "\n" + + "See AndroidXGradleProperties.kt if you need to add this property to " + + "the list of known properties." + throw GradleException(message) + } + } + } +} + +/** + * Returns whether tests in the project should display output. Build server scripts generally set + * displayTestOutput to false so that their failing test results aren't considered build failures, + * and instead pass their test failures on via build artifacts to be tracked and displayed on test + * dashboards in a different format + */ +fun Project.isDisplayTestOutput(): Boolean = findBooleanProperty(DISPLAY_TEST_OUTPUT) ?: true + +/** + * Returns whether the project should write versioned API files, e.g. `1.1.0-alpha01.txt`. + * + *

+ * When set to `true`, the `updateApi` task will write the current API surface to both `current.txt` + * and `.txt`. When set to `false`, only `current.txt` will be written. The default value + * is `true`. + */ +fun Project.isWriteVersionedApiFilesEnabled(): Boolean = + findBooleanProperty(WRITE_VERSIONED_API_FILES) ?: true + +/** Returns whether the build is for checking forward compatibility across projects */ +fun Project.usingMaxDepVersions(): Provider { + return project.providers.gradleProperty(USE_MAX_DEP_VERSIONS).map { true }.orElse(false) +} + +/** Returns whether we should use the offline mirror for dependencies */ +fun Project.useYarnOffline() = findBooleanProperty(YARN_OFFLINE_MODE) ?: false + +/** + * Returns whether this is an integration test that is allowing lint checks to be skipped to save + * configuration time. + */ +fun Project.allowMissingLintProject() = + findBooleanProperty(ALLOW_MISSING_LINT_CHECKS_PROJECT) ?: false + +fun Project.findBooleanProperty(propName: String): Boolean? = + project.providers.gradleProperty(propName).map { it.toBoolean() }.getOrNull() diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt new file mode 100644 index 0000000000000..41cabea6deb29 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt @@ -0,0 +1,1661 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.benchmark.gradle.BenchmarkPlugin +import androidx.build.AndroidXImplPlugin.Companion.TASK_TIMEOUT_MINUTES +import androidx.build.ProjectLayoutType.Companion.isJetBrainsFork +import androidx.build.Release.DEFAULT_PUBLISH_CONFIG +import androidx.build.buildInfo.addCreateLibraryBuildInfoFileTasks +import androidx.build.checkapi.AndroidMultiplatformApiTaskConfig +import androidx.build.checkapi.JavaApiTaskConfig +import androidx.build.checkapi.KmpApiTaskConfig +import androidx.build.checkapi.LibraryApiTaskConfig +import androidx.build.checkapi.configureProjectForApiTasks +import androidx.build.dependencyTracker.AffectedModuleDetector +import androidx.build.docs.CheckTipOfTreeDocsTask.Companion.setUpCheckDocsTask +import androidx.build.gitclient.getHeadShaProvider +import androidx.build.gradle.isRoot +import androidx.build.kythe.configureProjectForKzipTasks +import androidx.build.license.addLicensesToPublishedArtifacts +import androidx.build.lint.ValidateLintChecks +import androidx.build.resources.configurePublicResourcesStub +import androidx.build.sbom.configureSbomPublishing +import androidx.build.sbom.validateAllArchiveInputsRecognized +import androidx.build.sources.configureMultiplatformSourcesForAndroid +import androidx.build.sources.configureSourceJarForAndroid +import androidx.build.sources.configureSourceJarForJava +import androidx.build.sources.configureSourceJarForMultiplatform +import androidx.build.sources.registerValidateMultiplatformSourceSetNamingTask +import androidx.build.studio.StudioTask +import androidx.build.testConfiguration.addAppApkToTestConfigGeneration +import androidx.build.testConfiguration.addToModuleInfo +import androidx.build.testConfiguration.configureTestConfigGeneration +import androidx.build.uptodatedness.TaskUpToDateValidator +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.attributes.BuildTypeAttr +import com.android.build.api.dsl.AarMetadata +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.KotlinMultiplatformAndroidDeviceTestCompilation +import com.android.build.api.dsl.KotlinMultiplatformAndroidHostTestCompilation +import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget +import com.android.build.api.dsl.LibraryExtension +import com.android.build.api.dsl.TestBuildType +import com.android.build.api.dsl.TestExtension +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.HasDeviceTests +import com.android.build.api.variant.HasUnitTestBuilder +import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.api.variant.LibraryVariant +import com.android.build.api.variant.LibraryVariantBuilder +import com.android.build.gradle.AppPlugin +import com.android.build.gradle.LibraryPlugin +import com.android.build.gradle.TestPlugin +import com.android.build.gradle.api.KotlinMultiplatformAndroidPlugin +import com.google.devtools.ksp.gradle.KspExtension +import com.google.devtools.ksp.gradle.KspGradleSubplugin +import com.google.protobuf.gradle.ProtobufExtension +import com.google.protobuf.gradle.ProtobufPlugin +import java.io.File +import java.time.Duration +import java.util.Locale +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.JavaVersion +import org.gradle.api.JavaVersion.VERSION_11 +import org.gradle.api.JavaVersion.VERSION_17 +import org.gradle.api.JavaVersion.VERSION_1_8 +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.CacheableRule +import org.gradle.api.artifacts.ComponentMetadataContext +import org.gradle.api.artifacts.ComponentMetadataRule +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ExternalDependency +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.Usage +import org.gradle.api.configuration.BuildFeatures +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.bundling.Zip +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.api.tasks.testing.AbstractTestTask +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.gradle.build.event.BuildEventsListenerRegistry +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.withModule +import org.gradle.kotlin.dsl.withType +import org.gradle.plugin.devel.plugins.JavaGradlePluginPlugin +import org.gradle.plugin.devel.tasks.ValidatePlugins +import org.gradle.process.CommandLineArgumentProvider +import org.jetbrains.androidx.build.jetBrainsGetDefaultAndroidBaseJavaVersion +import org.jetbrains.androidx.build.jetBrainsGetDefaultTargetJavaVersion +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.plugin.KotlinBaseApiPlugin +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + +/** + * A plugin which enables all of the Gradle customizations for AndroidX. This plugin reacts to other + * plugins being added and adds required and optional functionality. + */ +abstract class AndroidXImplPlugin @Inject constructor() : Plugin { + @get:Inject abstract val registry: BuildEventsListenerRegistry + @get:Inject abstract val buildFeatures: BuildFeatures + + override fun apply(project: Project) { + if (project.isRoot) + throw Exception("Root project should use AndroidXRootImplPlugin instead") + val androidXExtension = initializeAndroidXExtension(project) + + val androidXKmpExtension = + project.extensions.create( + AndroidXMultiplatformExtension.EXTENSION_NAME, + project, + ) + + project.tasks.register(BUILD_ON_SERVER_TASK, DefaultTask::class.java) + // Perform different actions based on which plugins have been applied to the project. + // Many of the actions overlap, ex. API tracking. + project.plugins.configureEach { plugin -> + when (plugin) { + is JavaGradlePluginPlugin -> configureGradlePluginPlugin(project) + is JavaPlugin -> configureWithJavaPlugin(project, androidXExtension) + is LibraryPlugin -> configureWithLibraryPlugin(project, androidXExtension) + is AppPlugin -> configureWithAppPlugin(project, androidXExtension) + is TestPlugin -> configureWithTestPlugin(project, androidXExtension) + is KspGradleSubplugin -> configureWithKspPlugin(project) + is KotlinMultiplatformAndroidPlugin -> + configureWithKotlinMultiplatformAndroidPlugin( + project, + androidXKmpExtension.agpKmpExtension, + androidXExtension, + ) + is KotlinBasePluginWrapper, + is KotlinBaseApiPlugin -> + configureWithKotlinPlugin( + project, + androidXExtension, + plugin, + androidXKmpExtension, + ) + is ProtobufPlugin -> configureProtobufPlugin(project) + } + } + + project.configureLint() + project.configureKtfmt() + project.configureKotlinVersion() + project.configureJavaFormat() + + // Avoid conflicts between full Guava and LF-only Guava. + project.configureGuavaUpgradeHandler() + + // Configure all Jar-packing tasks for hermetic builds. + project.tasks.withType(Zip::class.java).configureEach { it.configureForHermeticBuild() } + project.tasks.withType(Copy::class.java).configureEach { it.configureForHermeticBuild() } + + val allHostTests = project.tasks.register("allHostTests") + // copy host side test results to DIST + project.tasks.withType(AbstractTestTask::class.java) { task -> + configureTestTask(project, task, allHostTests, androidXExtension) + } + + project.configureTaskTimeouts() + project.configureMavenArtifactUpload(androidXExtension, androidXKmpExtension) { + if (buildFeatures.isIsolatedProjectsEnabled()) return@configureMavenArtifactUpload + project.addCreateLibraryBuildInfoFileTasks(androidXExtension, androidXKmpExtension) + } + project.publishInspectionArtifacts() + project.configureProjectStructureValidation(androidXExtension) + project.configureProjectVersionValidation(androidXExtension) + project.validateMultiplatformPluginHasNotBeenApplied() + + project.tasks.register("printCoordinates", PrintProjectCoordinatesTask::class.java) { + it.configureWithAndroidXExtension(androidXExtension) + } + project.configureConstraintsWithinGroup(androidXExtension) + project.validateProjectParser(androidXExtension) + project.validateAllArchiveInputsRecognized() + project.afterEvaluate { + if (androidXExtension.shouldPublishSbom().get()) { + project.configureSbomPublishing(androidXExtension.isIsolatedProjectsEnabled()) + } + if (androidXExtension.shouldPublish.get()) { + project.validatePublishedMultiplatformHasDefault() + project.addLicensesToPublishedArtifacts(androidXExtension.license) + project.registerValidateRelocatedDependenciesTask() + } + project.registerValidateMultiplatformSourceSetNamingTask() + project.validateLintVersionTestExists(androidXExtension) + } + TaskUpToDateValidator.setup(project, registry) + + project.workaroundAndroidXDependencyResolutions() + project.configureSamplesProject() + project.configureMaxDepVersions(androidXExtension) + project.configureUnzipChromeBuildService() + + project.configureDependencyAnalysisPlugin() + } + + private fun initializeAndroidXExtension(project: Project): AndroidXExtension { + val versionService = LibraryVersionsService.registerOrGet(project).get() + val listProjectsService = ListProjectsService.registerOrGet(project) + return project.extensions + .create( + EXTENSION_NAME, + project, + versionService.libraryVersions, + versionService.libraryGroups.values.toList(), + versionService.libraryGroupsByGroupId, + versionService.overrideLibraryGroupsByProjectPath, + listProjectsService.map { it.allPossibleProjects }, + { project.getHeadShaProvider() }, + { configurationName: String -> + configureAarAsJarForConfiguration(project, configurationName) + }, + ) + .apply { kotlinTarget.set(KotlinTarget.DEFAULT) } + } + + /** + * Disables timestamps and ensures filesystem-independent archive ordering to maximize + * cross-machine byte-for-byte reproducibility of artifacts. + */ + private fun Zip.configureForHermeticBuild() { + isReproducibleFileOrder = true + isPreserveFileTimestamps = false + } + + private fun Copy.configureForHermeticBuild() { + duplicatesStrategy = DuplicatesStrategy.FAIL + } + + private fun configureTestTask( + project: Project, + task: AbstractTestTask, + anchorTask: TaskProvider, + androidXExtension: AndroidXExtension, + ) { + if (isJetBrainsFork(project)) return + anchorTask.configure { it.dependsOn(task) } + val xmlReportDestDir = project.getHostTestResultDirectory() + val testName = "${project.path}:${task.name}" + project.addToModuleInfo(testName, buildFeatures.isIsolatedProjectsEnabled()) + androidXExtension.testModuleNames.add(testName) + val archiveName = "$testName.zip" + if (project.isDisplayTestOutput()) { + // Enable tracing to see results in command line + task.testLogging.apply { + events = + hashSetOf(TestLogEvent.FAILED, TestLogEvent.SKIPPED, TestLogEvent.STANDARD_OUT) + showExceptions = true + showCauses = true + showStackTraces = true + exceptionFormat = TestExceptionFormat.FULL + } + } else { + task.testLogging.apply { + showExceptions = false + // Disable all output, including the names of the failing tests, by specifying + // that the minimum granularity we're interested in is this very high number + // (which is higher than the current maximum granularity that Gradle offers (3)) + minGranularity = 1000 + } + val testTaskName = task.name + val capitalizedTestTaskName = + testTaskName.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + val xmlReport = task.reports.junitXml + if (xmlReport.required.get()) { + val zipXmlTask = + project.tasks.register( + "zipXmlResultsOf$capitalizedTestTaskName", + Zip::class.java, + ) { + it.destinationDirectory.set(xmlReportDestDir) + it.archiveFileName.set(archiveName) + it.from(project.file(xmlReport.outputLocation)) + it.include("*.xml") + AffectedModuleDetector.configureTaskGuard(it) + } + task.finalizedBy(zipXmlTask) + } + } + } + + /** Configures the project to use the Kotlin version specified by `androidx.kotlinTarget`. */ + private fun Project.configureKotlinVersion() { + val kotlinVersionStringProvider = androidXConfiguration.kotlinBomVersion + + // Resolve unspecified Kotlin versions to the target version. + // TODO(b/443037365): Remove when bug fixed as built-in Kotlin would handle this + configurations.configureEach { configuration -> + configuration.withDependencies { dependencySet -> + dependencySet.filterIsInstance().forEach { dependency -> + if ( + dependency.group == "org.jetbrains.kotlin" && + dependency.version.isNullOrEmpty() + ) { + project.dependencies.constraints.add( + configuration.name, + dependency.module.toString(), + ) { + it.version { constraint -> + constraint.require(kotlinVersionStringProvider.get()) + } + } + } + } + } + } + + fun Provider.toKotlinVersionProvider() = map { version -> + KotlinVersion.fromVersion(version.substringBeforeLast('.')) + } + + // Set the Kotlin compiler's API and language version to ensure bytecode is compatible. + val kotlinVersionProvider = kotlinVersionStringProvider.toKotlinVersionProvider() + tasks.configureEach { task -> + if (task is KotlinCompilationTask<*>) { + task.compilerOptions.apiVersion.set(kotlinVersionProvider) + task.compilerOptions.languageVersion.set(kotlinVersionProvider) + } + } + + // Specify coreLibrariesVersion for consumption by Kotlin Gradle Plugin. Note that KGP does + // not explicitly support varying the version between tasks/configurations for a given + // project, so this is not strictly correct. Picking the non-test (e.g. lower) value seems + // to work, though. + afterEvaluate { evaluatedProject -> + evaluatedProject.kotlinExtensionOrNull?.let { kotlinExtension -> + kotlinExtension.coreLibrariesVersion = kotlinVersionStringProvider.get() + } + if (evaluatedProject.androidXExtension.shouldPublish.get()) { + tasks.register( + CheckKotlinApiTargetTask.TASK_NAME, + CheckKotlinApiTargetTask::class.java, + ) { + it.kotlinTarget.set(kotlinVersionProvider) + it.outputFile.set(layout.buildDirectory.file("kotlinApiTargetCheckReport.txt")) + } + addToBuildOnServer(CheckKotlinApiTargetTask.TASK_NAME) + } + } + + // Resolve classpath conflicts caused by kotlin-stdlib-jdk7 and -jdk8 artifacts by amending + // the kotlin-stdlib artifact metadata to add same-version constraints. + project.dependencies { + components { componentMetadata -> + componentMetadata.withModule( + "org.jetbrains.kotlin:kotlin-stdlib" + ) + } + } + } + + @CacheableRule + internal abstract class KotlinStdlibDependenciesRule : ComponentMetadataRule { + override fun execute(context: ComponentMetadataContext) { + val module = context.details.id + val version = module.version + context.details.allVariants { variantMetadata -> + variantMetadata.withDependencyConstraints { constraintsMetadata -> + val reason = "${module.name} is in atomic group ${module.group}" + constraintsMetadata.add("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$version") { + it.because(reason) + } + constraintsMetadata.add("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$version") { + it.because(reason) + } + } + } + } + } + + private fun configureWithKotlinPlugin( + project: Project, + androidXExtension: AndroidXExtension, + plugin: Any, + androidXMultiplatformExtension: AndroidXMultiplatformExtension, + ) { + val targetsAndroid = + project.provider { + project.plugins.hasPlugin(LibraryPlugin::class.java) || + project.plugins.hasPlugin(AppPlugin::class.java) || + project.plugins.hasPlugin(TestPlugin::class.java) || + project.plugins.hasPlugin(KotlinMultiplatformAndroidPlugin::class.java) + } + val defaultJavaTargetVersion = + androidXExtension.type.map { + jetBrainsGetDefaultTargetJavaVersion(it, project).toString() + } + val defaultJvmTarget = defaultJavaTargetVersion.map { JvmTarget.fromTarget(it) } + if (plugin is KotlinMultiplatformPluginWrapper) { + project.extensions.getByType().apply { + targets.withType().configureEach { t -> + t.compilations.configureEach { compilation -> + // Replace with compilation.compileJavaTaskProvider?.configure {} + // when b/438995010 is fixed + @Suppress("DEPRECATION") + compilation.compilerOptions.configure { jvmTarget.set(defaultJvmTarget) } + compilation.compileTaskProvider.configure { + it.compilerOptions.jvmTarget.set(defaultJvmTarget) + } + } + } + targets.withType(KotlinJvmTarget::class.java).configureEach { target -> + val defaultTargetVersionForNonAndroidTargets = + androidXExtension.type.map { + jetBrainsGetDefaultTargetJavaVersion( + softwareType = it, + project = project, + targetName = target.name, + ) + .toString() + } + val defaultJvmTargetForNonAndroidTargets = + defaultTargetVersionForNonAndroidTargets.map { JvmTarget.fromTarget(it) } + target.compilations.configureEach { compilation -> + compilation.compileJavaTaskProvider?.configure { javaCompile -> + javaCompile.targetCompatibility = + defaultTargetVersionForNonAndroidTargets.get() + javaCompile.sourceCompatibility = + defaultTargetVersionForNonAndroidTargets.get() + } + compilation.compileTaskProvider.configure { kotlinCompile -> + kotlinCompile.compilerOptions { + jvmTarget.set(defaultJvmTargetForNonAndroidTargets) + // Set jdk-release version for non-Android KMP targets + freeCompilerArgs.add( + defaultTargetVersionForNonAndroidTargets.map { + "-Xjdk-release=$it" + } + ) + } + } + } + } + } + } else { + project.tasks.withType(KotlinJvmCompile::class.java).configureEach { task -> + task.compilerOptions.jvmTarget.set(defaultJvmTarget) + task.compilerOptions.freeCompilerArgs.addAll( + targetsAndroid.zip(defaultJavaTargetVersion) { targetsAndroid, version -> + if (targetsAndroid) { + emptyList() + } else { + // Set jdk-release version for non-Android JVM projects + listOf("-Xjdk-release=$version") + } + } + ) + } + } + project.tasks.withType(KotlinCompile::class.java).configureEach { task -> + val kotlinCompilerArgs = + project.provider { + val args = + mutableListOf( + "-Xskip-metadata-version-check", + "-jvm-default=no-compatibility", + ) + if (androidXExtension.type.get().targetsKotlinConsumersOnly) { + // The Kotlin Compiler adds intrinsic assertions which are only relevant + // when the code is consumed by Java users. Therefore we can turn this off + // when code is being consumed by Kotlin users. + + // Additional Context: + // https://github.com/JetBrains/kotlin/blob/master/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JVMCompilerArguments.kt#L239 + // b/280633711 + args += + listOf( + "-Xno-param-assertions", + "-Xno-call-assertions", + "-Xno-receiver-assertions", + ) + } + + args + } + task.compilerOptions.freeCompilerArgs.addAll(kotlinCompilerArgs) + } + if (plugin is KotlinMultiplatformPluginWrapper) { + KonanPrebuiltsSetup.configureKonanDirectory(project) + project.afterEvaluate { + val libraryExtension = project.extensions.findByType() + if (libraryExtension != null) { + libraryExtension.configureAndroidLibraryWithMultiplatformPluginOptions() + } else if (!androidXMultiplatformExtension.hasAndroidMultiplatform()) { + // Kotlin MPP does not apply java plugin anymore, but we still want to configure + // all java-related tasks. + // We only need to do this when project does not have Android plugin, which + // already + // configures Java tasks. + configureWithJavaPlugin(project, androidXExtension) + } + } + project.configureKmp() + project.configureSourceJarForMultiplatform() + + // Disable any source JAR task(s) added by KotlinMultiplatformPlugin. + // https://youtrack.jetbrains.com/issue/KT-55881 + project.tasks.withType(Jar::class.java).configureEach { jarTask -> + if (jarTask.name == "androidSourcesJar" || jarTask.name == "jvmSourcesJar") { + // We can't set duplicatesStrategy directly on the Jar task since it will get + // overridden when the KotlinMultiplatformPlugin creates child specs, but we + // can set it on a per-file basis. + jarTask.eachFile { fileCopyDetails -> + fileCopyDetails.duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } + } + } + } + + project.afterEvaluate { + val kotlinExtension = project.kotlinExtensionOrNull + kotlinExtension?.explicitApi = + if (androidXExtension.shouldEnforceKotlinStrictApiMode().get()) { + ExplicitApiMode.Strict + } else { + ExplicitApiMode.Disabled + } + if (plugin is KotlinBaseApiPlugin) { + // TODO(b/443080559): Remove when built-in Kotlin adds kotlin-test-junit + // automatically + (kotlinExtension as KotlinAndroidProjectExtension) + .target + .compilations + .configureEach { compilation -> + if (!compilation.name.contains("test", ignoreCase = true)) + return@configureEach + compilation.defaultSourceSet.dependencies { + implementation(kotlin("test-junit")) + } + } + } + } + } + + private fun configureWithAppPlugin(project: Project, androidXExtension: AndroidXExtension) { + project.extensions.getByType().apply { + configureAndroidBaseOptions(project, androidXExtension) + defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk + val debugSigningConfig = signingConfigs.getByName("debug") + // Use a local debug keystore to avoid build server issues. + debugSigningConfig.storeFile = project.getKeystore() + buildTypes.configureEach { buildType -> + // Sign all the builds (including release) with debug key + buildType.signingConfig = debugSigningConfig + } + configureAndroidApplicationOptions(project, androidXExtension) + excludeVersionFiles(packaging.resources) + } + + project.extensions.getByType().apply { + beforeVariants(selector().withBuildType("release")) { variant -> + // Cast is needed because ApplicationAndroidComponentsExtension implements both + // HasUnitTestBuilder and VariantBuilder, and VariantBuilder#enableUnitTest is + // deprecated in favor of HasUnitTestBuilder#enableUnitTest. + // Remove the cast when we upgrade to AGP 9.0.0 + (variant as HasUnitTestBuilder).enableUnitTest = false + } + onVariants { it.configureTests(project.getKeystore()) } + } + + project.configureJavaCompilationWarnings( + androidXExtension = androidXExtension, + isTestApp = true, + ) + project.buildOnServerDependsOnAssembleRelease() + } + + private fun configureWithTestPlugin(project: Project, androidXExtension: AndroidXExtension) { + project.extensions.getByType().apply { + configureAndroidBaseOptions(project, androidXExtension) + defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk + val debugSigningConfig = signingConfigs.getByName("debug") + // Use a local debug keystore to avoid build server issues. + debugSigningConfig.storeFile = project.getKeystore() + buildTypes.configureEach { buildType -> + // Sign all the builds (including release) with debug key + buildType.signingConfig = debugSigningConfig + } + project.configureTestConfigGeneration( + androidXExtension.isIsolatedProjectsEnabled(), + androidXExtension, + ) + project.addAppApkToTestConfigGeneration(androidXExtension) + excludeVersionFiles(packaging.resources) + } + project.configureJavaCompilationWarnings(androidXExtension) + } + + private fun configureWithKspPlugin(project: Project) = + project.extensions.getByType().useKsp2.set(true) + + private fun configureCommonAndroidLibrary( + project: Project, + androidXExtension: AndroidXExtension, + androidComponents: + AndroidComponentsExtension<*, out LibraryVariantBuilder, out LibraryVariant>, + ) { + androidComponents.onVariants { variant -> + variant.configureTests(project.getKeystore()) + variant.enableMicrobenchmarkInternalDefaults(project) + project.validateKotlinModuleFiles( + variant.name, + variant.artifacts.get(SingleArtifact.AAR), + ) + } + + project.disableStrictVersionConstraints() + project.configureJavaCompilationWarnings(androidXExtension) + project.setUpCheckDocsTask(androidXExtension) + } + + private fun KotlinSourceSet.includesSourceSet(otherName: String): Boolean = + name == otherName || dependsOn.any { it.includesSourceSet(otherName) } + + private fun AarMetadata.configure(compileSdk: Int?) { + // Taken from + // https://developer.android.com/build/releases/gradle-plugin#api-level-support + fun mapToMinAgpVersion(compileSdk: Int): String { + return when (compileSdk) { + 33 -> "7.2.0" + 34 -> "8.1.1" + 35 -> "8.6.0" + 36 -> "8.9.1" + 37 -> "9.1.0" + else -> throw Exception("Unknown compileSdk to minAgpVersion mapping") + } + } + + // Propagate the compileSdk value into minCompileSdk. Don't propagate + // compileSdkExtension, since only one library actually depends on the extension + // APIs and they can explicitly declare that in their build.gradle. Note that when + // we're using a preview SDK, the value for compileSdk will be null and the + // resulting AAR metadata won't have a minCompileSdk -- + // this is okay because AGP automatically embeds forceCompileSdkPreview in the AAR + // metadata and uses it instead of minCompileSdk. + if (compileSdk == null) return + minCompileSdk = compileSdk + minAgpVersion = mapToMinAgpVersion(compileSdk) + } + + private fun configureWithKotlinMultiplatformAndroidPlugin( + project: Project, + kotlinMultiplatformAndroidTarget: KotlinMultiplatformAndroidLibraryTarget, + androidXExtension: AndroidXExtension, + ) { + val kotlinMultiplatformAndroidComponentsExtension = + project.extensions.getByType() + kotlinMultiplatformAndroidTarget.configureAndroidBaseOptions( + project, + kotlinMultiplatformAndroidComponentsExtension, + androidXExtension, + ) + configureCommonAndroidLibrary( + project, + androidXExtension, + kotlinMultiplatformAndroidComponentsExtension, + ) + kotlinMultiplatformAndroidComponentsExtension.apply { + finalizeDsl { + it.aarMetadata.configure(it.compileSdk) + it.lint.targetSdk = project.defaultAndroidConfig.targetSdk + project.setUpBlankProguardFileForKmpAarIfNeeded( + kotlinMultiplatformAndroidTarget.optimization.consumerKeepRules + ) + } + } + + kotlinMultiplatformAndroidComponentsExtension.onVariants { variant -> + project.configureProjectForApiTasks( + AndroidMultiplatformApiTaskConfig(variant), + androidXExtension, + ) + project.configureProjectForKzipTasks( + AndroidMultiplatformApiTaskConfig(variant), + androidXExtension, + ) + project.configurePublicResourcesStub(variant) + project.configureMultiplatformSourcesForAndroid(androidXExtension.samplesProjects) + } + + project.configureVersionFileWriter(project.multiplatformExtension!!, androidXExtension) + + project.configureDependencyVerification(androidXExtension) { taskProvider -> + kotlinMultiplatformAndroidTarget.compilations.configureEach { + taskProvider.configure { task -> task.dependsOn(it.compileTaskProvider) } + } + } + project.afterEvaluate { + project.addToBuildOnServer("assembleAndroidMain") + project.addToBuildOnServer("lint") + // Created to be consumed by docs-tip-of-tree + project.configurations.register("androidIntermediates") { + it.isCanBeResolved = false + it.attributes.attribute( + Usage.USAGE_ATTRIBUTE, + project.objects.named(Usage.JAVA_RUNTIME), + ) + it.attributes.attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category.LIBRARY), + ) + it.attributes.attribute( + BuildTypeAttr.ATTRIBUTE, + project.objects.named("release"), + ) + // disable, as it triggers android compilation during IDEA sync + if (!isJetBrainsFork(project)) it.outgoing.artifact(project.tasks.named("createFullJarAndroidMain")) + } + } + } + + private fun configureProtobufPlugin(project: Project) { + project.extensions.getByType(ProtobufExtension::class.java).apply { + protoc { it.artifact = project.getLibraryByName("protobufCompiler").toString() } + generateProtoTasks { + it.all().configureEach { task -> + // java projects have "java" output enabled, however Android projects do not + // so we need to create it for Android projects. + // https://github.com/google/protobuf-gradle-plugin?tab=readme-ov-file#default-outputs + val java = + if ( + project.plugins.hasPlugin("com.android.library") || + project.plugins.hasPlugin("com.android.application") + ) { + task.builtins.register("java") + } else task.builtins.named("java") + java.configure { options -> options.option("lite") } + } + } + } + } + + /** + * Excludes files telling which versions of androidx libraries were used in test apks, to avoid + * invalidating caches as often + */ + private fun excludeVersionFiles(packaging: com.android.build.api.variant.ResourcesPackaging) { + packaging.excludes.add("/META-INF/androidx*.version") + } + + /** + * Excludes files telling which versions of androidx libraries were used in test apks, to avoid + * invalidating caches as often + */ + private fun excludeVersionFiles(packaging: com.android.build.api.dsl.ResourcesPackaging) { + packaging.excludes.add("/META-INF/androidx*.version") + } + + private fun Project.buildOnServerDependsOnAssembleRelease() { + project.addToBuildOnServer("assembleRelease") + } + + private fun HasDeviceTests.configureTests(keystore: File) { + deviceTests.forEach { (_, deviceTest) -> + deviceTest.packaging.resources.apply { + excludeVersionFiles(this) + + // Workaround a limitation in AGP that fails to merge these META-INF license files. + pickFirsts.add("/META-INF/AL2.0") + // In addition to working around the above issue, we exclude the LGPL2.1 license as + // we're + // approved to distribute code via AL2.0 and the only dependencies which pull in + // LGPL2.1 + // are currently dual-licensed with AL2.0 and LGPL2.1. The affected dependencies + // are: + // - net.java.dev.jna:jna:5.5.0 + excludes.add("/META-INF/LGPL2.1") + + // AGP is unable to merge these and multiple artifacts ship this files + // e.g. org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar + // org/bouncycastle/bcprov-jdk18on/1.78.1/bcprov-jdk18on-1.78.1.jar + pickFirsts.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") + } + } + } + + private fun configureWithLibraryPlugin(project: Project, androidXExtension: AndroidXExtension) { + val buildTypeForTests = "release" + val libraryExtension = project.extensions.getByType() + libraryExtension.apply { + publishing { singleVariant(DEFAULT_PUBLISH_CONFIG) } + + configureAndroidBaseOptions(project, androidXExtension) + val debugSigningConfig = signingConfigs.getByName("debug") + // Use a local debug keystore to avoid build server issues. + debugSigningConfig.storeFile = project.getKeystore() + buildTypes.configureEach { buildType -> + // Sign all the builds (including release) with debug key + buildType.signingConfig = debugSigningConfig + } + testBuildType = buildTypeForTests + project.configureTestConfigGeneration( + androidXExtension.isIsolatedProjectsEnabled(), + androidXExtension, + ) + project.addAppApkToTestConfigGeneration(androidXExtension) + } + + val libraryAndroidComponentsExtension = + project.extensions.getByType() + configureCommonAndroidLibrary(project, androidXExtension, libraryAndroidComponentsExtension) + + libraryAndroidComponentsExtension.apply { + finalizeDsl { + it.defaultConfig.aarMetadata.configure(it.compileSdk) + project.setUpBlankProguardFileForAarIfNeeded(it.defaultConfig) + it.lint.targetSdk = project.defaultAndroidConfig.targetSdk + it.testOptions.targetSdk = project.defaultAndroidConfig.targetSdk + // Replace with a public API once available, see b/360392255 + it.buildTypes.configureEach { buildType -> + if (buildType.name == buildTypeForTests && !project.hasBenchmarkPlugin()) + (buildType as TestBuildType).isDebuggable = true + } + } + // Disable debug build type for Android Libraries + beforeVariants(selector().withBuildType("debug")) { variant -> variant.enable = false } + } + + project.configureVersionFileWriter(libraryAndroidComponentsExtension, androidXExtension) + + val prebuiltLibraries = listOf("libtracing_perfetto.so", "libc++_shared.so") + libraryAndroidComponentsExtension.onVariants { variant -> + if (variant.buildType == DEFAULT_PUBLISH_CONFIG) { + // Standard docs, resource API, and Metalava configuration for AndroidX projects. + project.configureProjectForApiTasks( + LibraryApiTaskConfig(variant), + androidXExtension, + ) + project.configureProjectForKzipTasks( + LibraryApiTaskConfig(variant), + androidXExtension, + ) + } + if (variant.name == DEFAULT_PUBLISH_CONFIG) { + project.configureSourceJarForAndroid(variant, androidXExtension.samplesProjects) + project.configurePublicResourcesStub(variant) + project.configureDependencyVerification(androidXExtension) { taskProvider -> + taskProvider.configure { task -> task.dependsOn("compileReleaseJavaWithJavac") } + } + } + val verifyELFRegionAlignmentTaskProvider = + project.tasks.register( + variant.name + "VerifyELFRegionAlignment", + VerifyELFRegionAlignmentTask::class.java, + ) { task -> + task.files.from( + variant.artifacts.get(SingleArtifact.MERGED_NATIVE_LIBS).map { dir -> + dir.asFileTree.files + .filter { it.extension == "so" } + .filter { it.path.contains("arm64-v8a") } + .filterNot { prebuiltLibraries.contains(it.name) } + } + ) + task.cacheEvenIfNoOutputs() + } + project.addToBuildOnServer(verifyELFRegionAlignmentTaskProvider) + } + project.buildOnServerDependsOnAssembleRelease() + } + + private fun configureGradlePluginPlugin(project: Project) { + project.tasks.withType(ValidatePlugins::class.java).configureEach { + it.enableStricterValidation.set(true) + it.failOnWarning.set(true) + } + project.addToBuildOnServer("validatePlugins") + SdkResourceGenerator.generateForHostTest(project) + } + + private fun configureWithJavaPlugin(project: Project, androidXExtension: AndroidXExtension) { + if ( + project.multiplatformExtension != null && + !project.multiplatformExtension!!.hasJvmTarget() + ) { + return + } + project.configureErrorProneForJava() + + // Force Java 1.8 source- and target-compatibility for all Java libraries. + val javaExtension = project.extensions.getByType() + project.afterEvaluate { + javaExtension.apply { + val defaultTargetJavaVersion = + jetBrainsGetDefaultTargetJavaVersion(androidXExtension.type.get(), project) + sourceCompatibility = defaultTargetJavaVersion + targetCompatibility = defaultTargetJavaVersion + } + if ( + !project.plugins.hasPlugin(KotlinBasePluginWrapper::class.java) || + !project.plugins.hasPlugin(KotlinBaseApiPlugin::class.java) + ) { + project.configureSourceJarForJava(androidXExtension.samplesProjects) + } + } + + project.setUpBlankProguardFileForJarIfNeeded(javaExtension) + project.configureJavaCompilationWarnings(androidXExtension) + + if ( + project.multiplatformExtension == null || + project.multiplatformExtension!!.hasJavaEnabled() + ) { + project.configureDependencyVerification(androidXExtension) { taskProvider -> + taskProvider.configure { task -> + task.dependsOn(project.tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME)) + } + } + } + + val apiTaskConfig = + if (project.multiplatformExtension != null) { + KmpApiTaskConfig + } else { + JavaApiTaskConfig + } + + project.configureProjectForApiTasks(apiTaskConfig, androidXExtension) + project.configureProjectForKzipTasks(apiTaskConfig, androidXExtension) + project.setUpCheckDocsTask(androidXExtension) + + if (project.multiplatformExtension == null) { + project.addToBuildOnServer("jar") + } else { + val multiplatformExtension = project.multiplatformExtension!! + multiplatformExtension.targets.forEach { + if (it.platformType == KotlinPlatformType.jvm) { + val task = project.tasks.named(it.artifactsTaskName, Jar::class.java) + project.addToBuildOnServer(task) + } + } + } + } + + private fun Project.configureProjectStructureValidation(androidXExtension: AndroidXExtension) { + if (isJetBrainsFork(project)) return + // AndroidXExtension.mavenGroup is not readable until afterEvaluate. + afterEvaluate { + val mavenGroup = androidXExtension.mavenGroup + val type = androidXExtension.type.get() + val isProbablyPublished = + type == SoftwareType.PUBLISHED_LIBRARY || + type == SoftwareType.PUBLISHED_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS + if ( + mavenGroup != null && isProbablyPublished && androidXExtension.shouldPublish.get() + ) { + validateProjectMavenGroup(mavenGroup.group) + validateProjectMavenName(androidXExtension.name.get(), mavenGroup.group) + validateProjectStructure(mavenGroup.group) + } + } + } + + private fun Project.configureProjectVersionValidation(androidXExtension: AndroidXExtension) { + // AndroidXExtension.mavenGroup is not readable until afterEvaluate. + afterEvaluate { androidXExtension.validateMavenVersion() } + } + + private fun Any.configureAndroidBaseOptions( + project: Project, + androidXExtension: AndroidXExtension, + ) { + // Workaround to avoid specifying the parametrized types of CommonExtension explicitly + // So we can clean up the parameters in AGP + // The compiler can infer that this is CommonExtension from these checks + if (this !is ApplicationExtension && this !is LibraryExtension && this !is TestExtension) { + throw IllegalArgumentException("Unexpected extension: $this") + } + compileOptions.apply { + sourceCompatibility = jetBrainsGetDefaultAndroidBaseJavaVersion(project) + targetCompatibility = jetBrainsGetDefaultAndroidBaseJavaVersion(project) + } + + val defaultMinSdk = project.defaultAndroidConfig.minSdk + + // Suppress output of android:compileSdkVersion and related attributes (b/277836549). + androidResources.additionalParameters += "--no-compile-sdk-metadata" + + compileSdk = project.defaultAndroidConfig.compileSdk + + buildToolsVersion = project.defaultAndroidConfig.buildToolsVersion + + defaultConfig.ndk.abiFilters.addAll(SUPPORTED_BUILD_ABIS) + defaultConfig.minSdk = defaultMinSdk + defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + testOptions.animationsDisabled = !project.isMacrobenchmark() + + project.afterEvaluate { + check( + !androidXExtension.shouldPublish.get() || + !compileOptions.isCoreLibraryDesugaringEnabled + ) { + "AndroidX libraries are not permitted to use core library desugaring as it " + + "forces library users to also enable core library desugaring." + } + + val minSdkVersion = defaultConfig.minSdk!! + check(minSdkVersion >= defaultMinSdk) { + "minSdkVersion $minSdkVersion lower than the default of $defaultMinSdk" + } + project.enforceBanOnVersionRanges() + + if (androidXExtension.type.get().compilationTarget != CompilationTarget.DEVICE) { + throw IllegalStateException( + "${androidXExtension.type.get().name} libraries cannot apply the android plugin, as" + + " they do not target android devices" + ) + } + } + + project.configureErrorProneForAndroid() + + // workaround for b/120487939 + project.configurations.configureEach { configuration -> + // Gradle seems to crash on androidtest configurations + // preferring project modules... + if (!configuration.name.lowercase(Locale.US).contains("androidtest")) { + configuration.resolutionStrategy.preferProjectModules() + } + } + + val componentsExtension = + project.extensions.getByType(AndroidComponentsExtension::class.java) + project.configureFtlRunner(componentsExtension) + + // If a dependency is missing a debug variant, use release instead. + buildTypes.getByName("debug").matchingFallbacks.add("release") + + // AGP warns if we use project.buildDir (or subdirs) for CMake's generated + // build files (ninja build files, CMakeCache.txt, etc.). Use a staging directory that + // lives alongside the project's buildDir. + @Suppress("DEPRECATION") + externalNativeBuild.cmake.buildStagingDirectory = + File(project.buildDir, "../nativeBuildStaging") + + // Align the ELF region of native shared libs 16kb boundary + defaultConfig.externalNativeBuild.cmake.arguments.add( + "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,-z,max-page-size=16384" + ) + } + + private fun KotlinMultiplatformAndroidLibraryTarget.configureAndroidBaseOptions( + project: Project, + componentsExtension: KotlinMultiplatformAndroidComponentsExtension, + androidXExtension: AndroidXExtension, + ) { + val defaultMinSdkVersion = project.defaultAndroidConfig.minSdk + val defaultCompileSdk = project.defaultAndroidConfig.compileSdk + + compileSdk = defaultCompileSdk + buildToolsVersion = project.defaultAndroidConfig.buildToolsVersion + + minSdk = defaultMinSdkVersion + + lint.targetSdk = project.defaultAndroidConfig.targetSdk + compilations + .withType(KotlinMultiplatformAndroidDeviceTestCompilation::class.java) + .configureEach { + it.instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + it.animationsDisabled = true + } + + withHostTestBuilder {} // enable Android host tests + withDeviceTestBuilder { sourceSetTreeName = "test" } + .configure { signing.storeFile = project.getKeystore() } + configureTargetSdkForTests(project.defaultAndroidConfig.targetSdk) + + // validate that SDK versions haven't been altered during evaluation + project.afterEvaluate { + val minSdkVersion = minSdk!! + check(minSdkVersion >= defaultMinSdkVersion) { + "minSdkVersion $minSdkVersion lower than the default of $defaultMinSdkVersion" + } + project.enforceBanOnVersionRanges() + } + + project.configureTestConfigGeneration( + buildFeatures.isIsolatedProjectsEnabled(), + androidXExtension, + ) + project.configureFtlRunner(componentsExtension) + } + + // TODO(b/425976012): Set targetSdkForTests to project.defaultAndroidConfig.targetSdk + private fun KotlinMultiplatformAndroidLibraryTarget.configureTargetSdkForTests(version: Int?) { + checkNotNull(version) { + "version must be set for tests. call `configureTargetSdkForTests` in the `finalizeDsl` block" + } + compilations + .withType(KotlinMultiplatformAndroidDeviceTestCompilation::class.java) + .configureEach { it.targetSdk { this.version = release(version) } } + + compilations + .withType(KotlinMultiplatformAndroidHostTestCompilation::class.java) + .configureEach { it.targetSdk { this.version = release(version.coerceAtMost(35)) } } + } + + /** + * Adds a module handler replacement rule that treats full Guava (of any version) as an upgrade + * to ListenableFuture-only Guava. This prevents irreconcilable versioning conflicts and/or + * class duplication issues. + */ + private fun Project.configureGuavaUpgradeHandler() { + // The full Guava artifact is very large, so they split off a special artifact containing a + // standalone version of the commonly-used ListenableFuture interface. However, they also + // structured the artifacts in a way that causes dependency resolution conflicts: + // - `com.google.guava:listenablefuture:1.0` contains only ListenableFuture + // - `com.google.guava:listenablefuture:9999.0` contains nothing + // - `com.google.guava:guava` contains all of Guava, including ListenableFuture + // If a transitive dependency includes `guava` as implementation-type and we have a direct + // API-type dependency on `listenablefuture:1.0`, then we'll get `listenablefuture:9999.0` + // on the compilation classpath -- which does not have the ListenableFuture class. However, + // if we tell Gradle to upgrade all LF dependencies to Guava then we'll get `guava` as an + // API-type dependency. See b/274621238 for more details. + project.dependencies { + modules { moduleHandler -> + moduleHandler.module("com.google.guava:listenablefuture") { module -> + module.replacedBy("com.google.guava:guava") + } + } + } + } + + private fun Project.disableStrictVersionConstraints() { + // Gradle inserts strict version constraints to ensure that dependency versions are + // identical across main and test source sets. For normal projects, this ensures + // that test bytecode is binary- and behavior-compatible with the main source set's + // bytecode. For AndroidX, though, we require backward compatibility and therefore + // don't need to enforce such constraints. + project.configurations.configureEach { configuration -> + if (!configuration.isTest()) return@configureEach + + configuration.dependencyConstraints.configureEach { dependencyConstraint -> + val strictVersion = dependencyConstraint.versionConstraint.strictVersion + if (strictVersion != "") { + // Migrate strict-type version constraints to required-type to allow upgrades. + dependencyConstraint.version { versionConstraint -> + versionConstraint.strictly("") + versionConstraint.require(strictVersion) + } + } + } + } + } + + private fun LibraryExtension.configureAndroidLibraryWithMultiplatformPluginOptions() { + sourceSets.findByName("main")!!.manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets + .findByName("androidTest")!! + .manifest + .srcFile("src/androidDeviceTest/AndroidManifest.xml") + } + + private fun Project.configureKmp() { + val kmpExtension = + checkNotNull(project.extensions.findByType()) { + """ + Project ${project.path} applies kotlin multiplatform plugin but we cannot find the + KotlinMultiplatformExtension. + """ + .trimIndent() + } + + kmpExtension.targets.configureEach { kotlinTarget -> + kotlinTarget.compilations.configureEach { compilation -> + // Configure all KMP targets to allow expect/actual classes that are not stable. + // (see https://youtrack.jetbrains.com/issue/KT-61573) + compilation.compileTaskProvider.configure { task -> + task.compilerOptions.freeCompilerArgs.add("-Xexpect-actual-classes") + androidXConfiguration.kotlinApiVersion.let { + task.compilerOptions.apiVersion.set(it) + task.compilerOptions.languageVersion.set(it) + } + } + } + } + } + + private fun ApplicationExtension.configureAndroidApplicationOptions( + project: Project, + androidXExtension: AndroidXExtension, + ) { + defaultConfig.apply { + versionCode = 1 + versionName = "1.0" + } + + project.configureTestConfigGeneration( + androidXExtension.isIsolatedProjectsEnabled(), + androidXExtension, + ) + project.addAppApkToTestConfigGeneration(androidXExtension) + project.addAppApkToFtlRunner() + } + + private fun Project.configureDependencyVerification( + androidXExtension: AndroidXExtension, + taskConfigurator: (TaskProvider) -> Unit, + ) { + if (buildFeatures.isIsolatedProjectsEnabled()) return + afterEvaluate { + if (androidXExtension.type.get().requiresDependencyVerification()) { + taskConfigurator(project.createVerifyDependencyVersionsTask()) + } + } + } + + // If this project wants other project in the same group to have the same version, + // this function configures those constraints. + private fun Project.configureConstraintsWithinGroup(androidXExtension: AndroidXExtension) { + if ( + !project.shouldAddGroupConstraints().get() || buildFeatures.isIsolatedProjectsEnabled() + ) { + return + } + project.afterEvaluate { + // make sure that the project has a group + val projectGroup = androidXExtension.mavenGroup ?: return@afterEvaluate + // make sure that this group is configured to use a single version + projectGroup.atomicGroupVersion ?: return@afterEvaluate + + // Under certain circumstances, a project is allowed to override its + // version see ( isGroupVersionOverrideAllowed ), in which case it's + // not participating in the versioning policy yet, + // and we don't assign it any version constraints + if (androidXExtension.mavenVersion != null) { + return@afterEvaluate + } + + // We don't want to emit the same constraint into our .module file more than once, + // and we don't want to try to apply a constraint to a configuration that doesn't accept + // them, + // so we create a configuration to hold the constraints and make each other constraint + // extend it + val constraintConfiguration = project.configurations.create("groupConstraints") + project.configurations.configureEach { configuration -> + if (configuration != constraintConfiguration) + configuration.extendsFrom(constraintConfiguration) + } + + val otherProjectsInSameGroup = androidXExtension.getOtherProjectsInSameGroup() + val constraints = project.dependencies.constraints + val allProjectsExist = buildContainsAllStandardProjects() + for (otherProject in otherProjectsInSameGroup) { + val otherGradlePath = otherProject.gradlePath + if (otherGradlePath == ":compose:ui:ui-android-stubs") { + // exemption for library that doesn't truly get published: b/168127161 + continue + } + // We only enable constraints for builds that we intend to be able to publish from. + // If a project isn't included in a build we intend to be able to publish from, + // the project isn't going to be published. + // Sometimes this can happen when a project subset is enabled: + // The KMP project subset enabled by androidx_multiplatform_mac.sh contains + // :benchmark:benchmark-common but not :benchmark:benchmark-benchmark + // This is ok because we don't intend to publish that artifact from that build + val otherProjectShouldExist = + allProjectsExist || findProject(otherGradlePath) != null + if (!otherProjectShouldExist) { + continue + } + // We only emit constraints referring to projects that will release + val otherFilepath = + getSupportRootFolder().resolve(File(otherProject.filePath, "build.gradle")) + val parsed = + if (otherFilepath.exists()) { + parseBuildFile(otherFilepath) + } else { + parseBuildFile( + getSupportRootFolder() + .resolve(File(otherProject.filePath, "build.gradle.kts")) + ) + } + if (!parsed.shouldRelease()) { + continue + } + if (parsed.softwareType == SoftwareType.SAMPLES) { + // a SAMPLES project knows how to publish, but we don't intend to actually + // publish it + continue + } + // Under certain circumstances, a project is allowed to override its + // version see ( isGroupVersionOverrideAllowed ), in which case it's + // not participating in the versioning policy yet and we don't emit + // version constraints referencing it + if (parsed.specifiesVersion) { + continue + } + val dependencyConstraint = project(otherGradlePath) + constraints.add(constraintConfiguration.name, dependencyConstraint) { + it.because("${project.name} is in atomic group ${projectGroup.group}") + } + } + + // disallow duplicate constraints + project.configurations.configureEach { config -> + // Allow duplicate constraints in test configurations. This is partially a + // workaround for duplication due to downgrading strict-type dependencies to + // required-type, but also we don't care if tests have duplicate constraints. + if (config.isTest()) return@configureEach + + // find all constraints contributed by this Configuration and its ancestors + val configurationConstraints: MutableSet = mutableSetOf() + config.hierarchy.forEach { parentConfig -> + parentConfig.dependencyConstraints.configureEach { dependencyConstraint -> + dependencyConstraint.apply { + if ( + versionConstraint.requiredVersion != "" && + versionConstraint.requiredVersion != "unspecified" + ) { + val key = + "${dependencyConstraint.group}:${dependencyConstraint.name}" + if (configurationConstraints.contains(key)) { + throw GradleException( + "Constraint on $key was added multiple times in " + + "$config (version = " + + "${versionConstraint.requiredVersion}).\n\n" + + "This is unnecessary and can also trigger " + + "https://github.com/gradle/gradle/issues/24037 in " + + "builds trying to use the resulting artifacts." + ) + } + configurationConstraints.add(key) + } + } + } + } + } + } + } + + /** + * Tells whether this build contains the usual set of all projects (`./gradlew projects`) + * Sometimes developers request to include fewer projects because this may run more quickly + */ + private fun Project.buildContainsAllStandardProjects(): Boolean { + if (getProjectSubset() != null) return false + if (ProjectLayoutType.isPlayground(this)) return false + return true + } + + companion object { + const val FINALIZE_TEST_CONFIGS_WITH_APKS_TASK = "finalizeTestConfigsWithApks" + const val ZIP_TEST_CONFIGS_WITH_APKS_TASK = "zipTestConfigsWithApks" + + const val TASK_GROUP_API = "API" + + const val EXTENSION_NAME = "androidx" + + // b/366238650 + val SUPPORTED_BUILD_ABIS = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") + + /** Fail the build if a non-Studio task runs longer than expected */ + const val TASK_TIMEOUT_MINUTES = 60L + } +} + +internal fun aospGetDefaultTargetJavaVersion( + softwareType: SoftwareType, + projectName: String? = null, + targetName: String? = null, +): JavaVersion { + return when { + // TODO(b/353328300): Move room-compiler-processing to Java 17 once Dagger is ready. + projectName != null && projectName.contains("room3-compiler-processing") -> VERSION_11 + projectName != null && projectName.contains("desktop") -> VERSION_11 + targetName != null && (targetName == "desktop" || targetName == "jvmStubs") -> VERSION_11 + softwareType.compilationTarget == CompilationTarget.HOST -> VERSION_17 + else -> VERSION_1_8 + } +} + +private fun Project.validateLintVersionTestExists(androidXExtension: AndroidXExtension) { + if (!androidXExtension.type.get().isLint()) { + return + } + kotlinExtensionOrNull?.let { extension -> + val validateLintChecks = + tasks.register("validateLintChecks", ValidateLintChecks::class.java) { task -> + task.cacheEvenIfNoOutputs() + task.sourceDirectories.from( + extension.sourceSets.flatMap { it.kotlin.sourceDirectories } + ) + } + addToBuildOnServer(validateLintChecks) + } +} + +/** Returns whether the configuration is used for testing. */ +private fun Configuration.isTest(): Boolean = name.lowercase().contains("test") + +/** Returns whether the configuration is part of publication. */ +internal fun Configuration.isPublished(): Boolean = + !isTest() && !name.lowercase().contains("metadata") && !name.endsWith("CInterop") + +internal val Project.androidExtension: AndroidComponentsExtension<*, *, *> + get() = + extensions.findByType() + ?: throw IllegalArgumentException("Failed to find any registered Android extension") + +val Project.multiplatformExtension + get() = extensions.findByType(KotlinMultiplatformExtension::class.java) + +val Project.kotlinExtensionOrNull: KotlinProjectExtension? + get() = extensions.findByType() + +val Project.androidXExtension: AndroidXExtension + get() = extensions.getByType() + +/** + * Configures all non-Studio tasks in a project (see b/153193718 for background) to time out after + * [TASK_TIMEOUT_MINUTES]. + */ +internal fun Project.configureTaskTimeouts() { + // A set of tasks that sometimes take >60 minutes. b/383874664 + val slowTasks = + setOf( + ":compose:ui:ui:compileReleaseAndroidTestKotlinAndroid", + ":compose:foundation:foundation:compileReleaseAndroidTestKotlinAndroid", + ":compose:foundation:foundation:integration-tests:lazy-tests:compileReleaseAndroidTestKotlin", + ) + tasks.configureEach { t -> + // skip adding a timeout for some tasks that both take a long time and + // that we can count on the user to monitor + if (t !is StudioTask) { + t.timeout.set( + Duration.ofMinutes(if (t.path in slowTasks) 80L else TASK_TIMEOUT_MINUTES) + ) + } + } +} + +private class JavaCompileArgumentProvider( + private val isTestApp: Boolean, + private val failOnDeprecationWarnings: Provider, + private val usingMaxDepVersions: Provider, +) : CommandLineArgumentProvider { + override fun asArguments(): List { + // JDK 21 considers Java 8 an obsolete source and target value. Disable this warning. + val args = mutableListOf("-Xlint:-options") + // If we're running a hypothetical test build confirming that tip-of-tree versions + // are compatible, then we're not concerned about warnings + if (!usingMaxDepVersions.get() && !isTestApp) { + args.add("-Xlint:unchecked") + if (failOnDeprecationWarnings.get()) { + args.add("-Xlint:deprecation") + } + } + return args + } +} + +private fun Project.configureJavaCompilationWarnings( + androidXExtension: AndroidXExtension, + isTestApp: Boolean = false, +) { + project.tasks.withType(JavaCompile::class.java).configureEach { task -> + task.options.compilerArgumentProviders.add( + JavaCompileArgumentProvider( + isTestApp = isTestApp, + failOnDeprecationWarnings = androidXExtension.failOnDeprecationWarnings, + usingMaxDepVersions = usingMaxDepVersions(), + ) + ) + } +} + +fun Project.hasBenchmarkPlugin(): Boolean { + return this.plugins.hasPlugin(BenchmarkPlugin::class.java) +} + +fun Project.isMacrobenchmark(): Boolean { + return this.path.endsWith("macrobenchmark") +} + +/** + * Returns a string that is a valid filename and loosely based on the project name The value + * returned for each project will be distinct + */ +fun String.asFilenamePrefix(): String { + return this.substring(1).replace(':', '-') +} + +/** + * Sets the specified [task] as a dependency of the top-level `check` task, ensuring that it runs as + * part of `./gradlew check`. + */ +fun Project.addToCheckTask(task: TaskProvider) { + project.tasks.named("check").configure { it.dependsOn(task) } +} + +fun Project.validateMultiplatformPluginHasNotBeenApplied() { + if (plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java)) { + throw GradleException( + "The Kotlin multiplatform plugin should only be applied by the AndroidX plugin." + ) + } +} + +/** Verifies that ProjectParser computes the correct values for this project */ +fun Project.validateProjectParser(androidXExtension: AndroidXExtension) { + if (isJetBrainsFork(project)) return + // If configuration fails, we don't want to validate the ProjectParser + // (otherwise it could report a confusing, unnecessary error) + project.gradle.taskGraph.whenReady { + val parsed = project.parse() + val errorPrefix = "ProjectParser error parsing ${project.path}." + check(androidXExtension.type.get() == parsed.softwareType) { + "$errorPrefix Incorrectly computed libraryType = ${parsed.softwareType} " + + "instead of ${androidXExtension.type.get()}" + } + check(androidXExtension.shouldPublish.get() == parsed.shouldPublish()) { + "$errorPrefix Incorrectly computed shouldPublish() = ${parsed.shouldPublish()} " + + "instead of ${androidXExtension.shouldPublish.get()}" + } + check(androidXExtension.shouldRelease.get() == parsed.shouldRelease()) { + "$errorPrefix Incorrectly computed shouldRelease() = ${parsed.shouldRelease()} " + + "instead of ${androidXExtension.shouldRelease.get()}" + } + check(androidXExtension.projectDirectlySpecifiesMavenVersion == parsed.specifiesVersion) { + "$errorPrefix Incorrectly computed specifiesVersion = ${parsed.specifiesVersion} " + + " instead of ${androidXExtension.projectDirectlySpecifiesMavenVersion}" + } + } +} + +/** Validates the Maven version against Jetpack guidelines. */ +fun AndroidXExtension.validateMavenVersion() { + val mavenGroup = mavenGroup + val mavenVersion = mavenVersion + val forcedVersion = mavenGroup?.atomicGroupVersion + if (forcedVersion != null && forcedVersion == mavenVersion) { + throw GradleException( + """ + Unnecessary override of same-group library version + + Project version is already set to $forcedVersion by same-version group + ${mavenGroup.group}. + + To fix this error, remove "mavenVersion = ..." from your build.gradle + configuration. + """ + .trimIndent() + ) + } +} + +/** Workarounds for configuration resolution */ +fun Project.workaroundAndroidXDependencyResolutions() { + project.configurations.configureEach { configuration -> + // https://github.com/gradle/gradle/issues/27407 + configuration.resolutionStrategy.preferProjectModules() + + // https://github.com/gradle/gradle/issues/7594 + configuration.resolutionStrategy.eachDependency { dependency -> + if (dependency.requested.group.startsWith("androidx.")) { + // Drop aar classifier that comes from aar in POM files + // as it causes a bug in Gradle. Gradle does not actually need the + // classifiers for Android libraries for them to work correctly. + dependency.artifactSelection { it.withoutArtifactSelectors() } + } + } + } +} + +private fun Project.configureUnzipChromeBuildService() { + if (ProjectLayoutType.isPlayground(this)) { + return + } + gradle.sharedServices.registerIfAbsent("unzipChrome", UnzipChromeBuildService::class.java) { + it.parameters.browserDir.set(File(getPrebuiltsRoot(), "androidx/chrome-for-testing/")) + it.parameters.unzipToDir.set(getOutDirectory().resolve("chrome-bin")) + } +} + +private fun Project.enforceBanOnVersionRanges() { + configurations.configureEach { configuration -> + configuration.resolutionStrategy.eachDependency { dep -> + val target = dep.target + val version = target.version + // Enforce the ban on declaring dependencies with version ranges. + // Note: In playground, this ban is exempted to allow unresolvable prebuilts + // to automatically get bumped to snapshot versions via version range + // substitution. + if ( + version != null && + Version.isDependencyRange(version) && + project.rootProject.rootDir == project.getSupportRootFolder() + ) { + throw IllegalArgumentException( + "Dependency ${dep.target} declares its version as " + + "version range ${dep.target.version} however the use of " + + "version ranges is not allowed, please update the " + + "dependency to list a fixed version." + ) + } + } + } +} + +internal fun Project.hasAndroidMultiplatformPlugin(): Boolean = + extensions.findByType(AndroidXMultiplatformExtension::class.java)?.hasAndroidMultiplatform() + ?: false + +@Suppress("DEPRECATION") +internal fun KotlinMultiplatformExtension.hasJavaEnabled(): Boolean = + targets.withType(KotlinJvmTarget::class.java).singleOrNull()?.withJavaEnabled ?: false + +internal fun KotlinMultiplatformExtension.hasJvmTarget(): Boolean = + targets.withType(KotlinJvmTarget::class.java).isEmpty().not() + +internal fun String.camelCase() = replaceFirstChar { + if (it.isLowerCase()) it.titlecase() else it.toString() +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt new file mode 100644 index 0000000000000..93252641162c1 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt @@ -0,0 +1,1054 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.clang.AndroidXClang +import androidx.build.clang.CombineObjectFilesTask +import androidx.build.clang.KonanBuildService +import androidx.build.clang.MultiTargetNativeCompilation +import androidx.build.clang.NativeLibraryBundler +import androidx.build.clang.configureCinterop +import com.android.build.api.dsl.KotlinMultiplatformAndroidCompilation +import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget +import com.android.build.gradle.api.KotlinMultiplatformAndroidPlugin +import groovy.lang.Closure +import java.io.File +import javax.inject.Inject +import org.gradle.api.Action +import org.gradle.api.GradleException +import org.gradle.api.NamedDomainObjectCollection +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.configuration.BuildFeatures +import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.the +import org.gradle.kotlin.dsl.withType +import org.jetbrains.androidx.build.configureForkWebTarget +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithHostTests +import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl +import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinWasmTargetDsl +import org.jetbrains.kotlin.gradle.targets.js.ir.DefaultIncrementalSyncTask +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnvSpec +import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin +import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootEnvSpec +import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget +import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenEnvSpec +import org.jetbrains.kotlin.gradle.targets.wasm.binaryen.BinaryenPlugin +import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsEnvSpec +import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsPlugin +import org.jetbrains.kotlin.gradle.targets.wasm.yarn.WasmYarnPlugin +import org.jetbrains.kotlin.gradle.targets.wasm.yarn.WasmYarnRootEnvSpec +import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile +import org.jetbrains.kotlin.konan.target.LinkerOutputKind + +/** + * [AndroidXMultiplatformExtension] is an extension that wraps specific functionality of the Kotlin + * multiplatform extension, and applies the Kotlin multiplatform plugin when it is used. The purpose + * of wrapping is to prevent targets from being added when the platform has not been enabled. e.g. + * the `macosX64` target is gated on a `project.enableMac` check. + */ +abstract class AndroidXMultiplatformExtension(val project: Project) { + + @get:Inject abstract val buildFeatures: BuildFeatures + + var enableBinaryCompatibilityValidator = true + + /* + * Adds a kotlin stdlib klib directory as an input to test tasks. + * This is specifically useful for BCV, but it needs to be set up by our buildSrc code to + * make sure we use the correct installation and don't accidentally cause something to be + * downloaded from the internet. + * + * Sets the `kotlin.stdlib.klib.dir` property which can be accessed inside the tests + */ + fun provideKlibStdLibForTests() { + val konanBuildService = KonanBuildService.obtain(project) + // directory format of stdlib klib for use during tests + val stdLibKlibDir = + konanBuildService.map { it.parameters.konanHome.dir("klib/common/stdlib") } + project.tasks.withType(Test::class.java).configureEach { task -> + task.inputs + .dir(stdLibKlibDir) + .withPropertyName("kotlinStdLib") + .withPathSensitivity(PathSensitivity.RELATIVE) + task.doFirst { + task.systemProperty( + "kotlin.stdlib.klib.dir", + stdLibKlibDir.get().get().asFile.absolutePath, + ) + } + } + } + + // Kotlin multiplatform plugin is only applied if at least one target / sourceset is added. + private val kotlinExtensionDelegate = lazy { + project.validateMultiplatformPluginHasNotBeenApplied() + project.plugins.apply(KotlinMultiplatformPluginWrapper::class.java) + project.multiplatformExtension!!.also { it.applyAndroidXDefaultHierarchyTemplate() } + } + private val kotlinExtension: KotlinMultiplatformExtension by kotlinExtensionDelegate + private val agpKmpExtensionDelegate = lazy { + // make sure to initialize the kotlin extension by accessing the property + val extension = (kotlinExtension as ExtensionAware) + project.plugins.apply(KotlinMultiplatformAndroidPlugin::class.java) + extension.extensions.getByType(KotlinMultiplatformAndroidLibraryTarget::class.java) + } + + val agpKmpExtension: KotlinMultiplatformAndroidLibraryTarget by agpKmpExtensionDelegate + + /** + * The list of platforms that have been declared as supported in the build configuration. + * + * This may be a superset of the currently enabled platforms in [targetPlatforms]. + */ + val supportedPlatforms: MutableSet = mutableSetOf() + + /** + * Artifact-redirection (parallel-graph back-end): one entry per concrete target declared inside a + * `redirect { }` block. Each entry names a target that the fork builds *empty* (an empty, + * but valid, klib/jar/aar depending on the `androidx.*` coordinate) by re-rooting its + * source-sets onto an empty parallel graph (`redirectCommonMain`) instead of the real + * `commonMain`. The JetBrains plugin reads this registry in `afterEvaluate`. + * + * `redirectCoordinate` carries the `androidx.*` group from the `redirect("group") { }` argument + * (required) and the optional version override; when its version is null the back-end resolves it + * from the `[versions]` table of `redirectversions.toml`. + */ + internal data class RedirectTargetDecl( + val targetName: String, + val redirectCoordinate: RedirectCoordinate + ) + + /** Targets registered for redirect via `redirect { }`. Consumed by the JetBrains plugin. */ + internal val redirectTargetDecls: MutableList = mutableListOf() + + /** + * Names of redirect targets, registered **before** the target is created (see `expectRedirect`). + * The hierarchy-template `excludeCompilations` predicate reads this to keep redirect targets out + * of the `commonMain` tree. Must be populated before the target's compilation is created, because + * the template evaluates the predicate at compilation-creation time. + */ + internal val redirectTargetNames: MutableSet = mutableSetOf() + + /** Pre-register expected redirect target names so the hierarchy predicate excludes them. */ + private fun expectRedirect(vararg names: String) { redirectTargetNames += names } + + /** The `androidx.*` coordinate a `redirect("group", version) { }` block points its targets at. */ + internal data class RedirectCoordinate(val group: String, val version: String?) + + // Ambient state for the `redirect { … }` scope: non-null while a redirect block is executing + // (holding that block's coordinate), null otherwise. A plain target function called inside the + // block sees it (via `potentiallyRedirecting`) and redirects its target to the coordinate instead + // of fork-building. + private var redirectCoordinate: RedirectCoordinate? = null + + /** + * Empty parallel root for redirect targets. Created lazily on the first redirect target (declared + * inside `redirect { }`) so that redirect leaves can be wired to it **at target-creation time** — + * this is what keeps them off the real `commonMain`. KGP applies the default hierarchy template + * only when a source-set + * has no manual `dependsOn` edge; adding one here (synchronously, during configuration) opts the + * redirect leaf out of the auto-wiring to `commonMain`. Doing this in `afterEvaluate` is too late + * (the dependsOn closure is computed reactively on edge add and is not recomputed on removal). + */ + private val redirectCommonMain: org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet by lazy { + kotlinExtension.sourceSets.maybeCreate("redirectCommonMain") + } + + private fun recordRedirect(target: KotlinTarget, targetName: String, redirectCoordinate: RedirectCoordinate) { + // Invariant: the name `potentiallyRedirecting` pre-registered must match the created target, + // otherwise the hierarchy predicate excluded the wrong name from `commonMain`. + assert(target.name == targetName) { + "redirect target name mismatch: expected '$targetName' but created target is '${target.name}'" + } + redirectTargetNames += target.name + redirectTargetDecls += RedirectTargetDecl(target.name, redirectCoordinate) + // Wire the target's main compilation source-set to the parallel root up-front. + target.compilations.findByName("main")?.defaultSourceSet?.dependsOn(redirectCommonMain) + } + + /** + * The list of platforms that are currently enabled. + * + * This will vary across build environments. For example, a project's build configuration may + * have requested `mac()` but this is not available when building on Linux. + */ + val targetPlatforms: List + get() = + if (kotlinExtensionDelegate.isInitialized()) { + kotlinExtension.targets.mapNotNull { + if (it.targetName != "metadata") { + it.targetName + } else { + null + } + } + } else { + throw GradleException("Kotlin multi-platform extension has not been initialized") + } + + /** + * Default platform identifier used for specifying POM dependencies. + * + * This platform will be added as a dependency to the multi-platform anchor artifact's POM + * publication. For example, if the anchor artifact is `collection` and the default platform is + * `jvm`, then the POM for `collection` will express a dependency on `collection-jvm`. This + * ensures that developers who are silently upgrade to KMP artifacts but are not using Gradle + * still see working artifacts. + * + * If no default was specified and a single platform is requested (ex. using [jvm]), returns the + * identifier for that platform. + */ + var defaultPlatform: String? = null + get() = field ?: supportedPlatforms.singleOrNull()?.id + set(value) { + if (value != null) { + if (supportedPlatforms.none { it.id == value }) { + throw GradleException( + "Platform $value has not been requested as a target. " + + "Available platforms are: " + + supportedPlatforms.joinToString(", ") { it.id } + ) + } + if (targetPlatforms.none { it == value }) { + throw GradleException( + "Platform $value is not available in this build " + + "environment. Available platforms are: " + + targetPlatforms.joinToString(", ") + ) + } + } + field = value + } + + val targets: NamedDomainObjectCollection + get() = kotlinExtension.targets + + /** Helper class to access Clang functionality. */ + private val clang = AndroidXClang(project) + + /** Helper class to bundle outputs of clang compilation into an AAR / JAR. */ + private val nativeLibraryBundler = NativeLibraryBundler(project) + + internal fun hasNativeTarget(): Boolean { + // it is important to check initialized here not to trigger initialization + return kotlinExtensionDelegate.isInitialized() && + targets.any { it.platformType == KotlinPlatformType.native } + } + + internal fun hasAndroidMultiplatform(): Boolean { + return agpKmpExtensionDelegate.isInitialized() + } + + fun sourceSets(closure: Closure<*>) { + if (kotlinExtensionDelegate.isInitialized()) { + kotlinExtension.sourceSets.configure(closure).also { + kotlinExtension.sourceSets.configureEach { sourceSet -> + if (sourceSet.name == "main" || sourceSet.name == "test") { + throw Exception( + "KMP-enabled projects must use target-prefixed " + + "source sets, e.g. androidMain or commonTest, rather than main or test" + ) + } + } + } + } + } + + /** + * Creates a multi-target native compilation with the given [archiveName]. + * + * The given [configure] action can be used to add targets, sources, includes etc. + * + * The outputs of this compilation is not added to any artifact by default. + * * To use the outputs via cinterop (kotlin native), use the [createCinterop] function. + * * To bundle the outputs inside a JAR (to be loaded at runtime), use the + * [addNativeLibrariesToResources] function. + * * To bundle the outputs inside an AAR (to be loaded at runtime), use the + * [addNativeLibrariesToJniLibs] function. + * + * @param archiveName The archive file name for the native artifacts (.so, .a or .o) + * @param outputKind The kind of output it should be produced (library or executable). + * @param configure Action block to configure the compilation. + */ + @JvmOverloads + fun createNativeCompilation( + archiveName: String, + outputKind: LinkerOutputKind = LinkerOutputKind.DYNAMIC_LIBRARY, + configure: Action, + ): MultiTargetNativeCompilation { + return clang.createNativeCompilation( + archiveName = archiveName, + configure = configure, + outputKind = outputKind, + ) + } + + /** + * Creates a Kotlin Native cinterop configuration for the given [nativeTarget] main compilation + * from the outputs of [nativeCompilation]. + * + * @param nativeTarget The kotlin native target for which a new cinterop will be added on the + * main compilation. + * @param nativeCompilation The [MultiTargetNativeCompilation] which will be embedded into the + * generated cinterop klib. + * @param cinteropName The name of the cinterop definition. A matching "" file + * needs to be present in the default cinterop location + * (src/nativeInterop/cinterop/). + */ + @JvmOverloads + fun createCinterop( + nativeTarget: KotlinNativeTarget, + nativeCompilation: MultiTargetNativeCompilation, + cinteropName: String = nativeCompilation.archiveName, + ) { + createCinterop( + kotlinNativeCompilation = + nativeTarget.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) + as KotlinNativeCompilation, + nativeCompilation = nativeCompilation, + cinteropName = cinteropName, + ) + } + + /** + * Creates a Kotlin Native cinterop configuration for the given [kotlinNativeCompilation] from + * the outputs of [nativeCompilation]. + * + * @param kotlinNativeCompilation The kotlin native compilation for which a new cinterop will be + * added + * @param nativeCompilation The [MultiTargetNativeCompilation] which will be embedded into the + * generated cinterop klib. + * @param cinteropName The name of the cinterop definition. A matching "" file + * needs to be present in the default cinterop location + * (src/nativeInterop/cinterop/). + */ + @JvmOverloads + fun createCinterop( + kotlinNativeCompilation: KotlinNativeCompilation, + nativeCompilation: MultiTargetNativeCompilation, + cinteropName: String = nativeCompilation.archiveName, + ) { + nativeCompilation.configureCinterop( + kotlinNativeCompilation = kotlinNativeCompilation, + cinteropName = cinteropName, + ) + } + + /** + * Creates a Kotlin Native cinterop configuration for the given [kotlinNativeCompilation] from + * the single output of a configuration. + * + * @param kotlinNativeCompilation The kotlin native compilation for which a new cinterop will be + * added + * @param configuration The configuration to resolve. It is expected for the configuration to + * contain a single file of the archive file to be referenced in the C interop definition + * file. + */ + fun createCinteropFromArchiveConfiguration( + kotlinNativeCompilation: KotlinNativeCompilation, + configuration: Configuration, + ) { + configureCinterop(project, kotlinNativeCompilation, configuration) + } + + /** + * Adds the native outputs from [nativeCompilation] to the assets of the [androidTarget]. + * + * @see CombineObjectFilesTask for details. + */ + @JvmOverloads + fun addNativeLibrariesToVariantAssets( + androidTarget: KotlinMultiplatformAndroidLibraryTarget, + nativeCompilation: MultiTargetNativeCompilation, + forTest: Boolean = false, + ) = + nativeLibraryBundler.addNativeLibrariesToAndroidVariantSources( + androidTarget = androidTarget, + nativeCompilation = nativeCompilation, + forTest = forTest, + provideSourceDirectories = { assets }, + ) + + /** + * Adds the native outputs from [nativeCompilation] to the jni libs dependency of the + * [androidTarget]. + * + * @see CombineObjectFilesTask for details. + */ + @JvmOverloads + fun addNativeLibrariesToJniLibs( + androidTarget: KotlinMultiplatformAndroidLibraryTarget, + nativeCompilation: MultiTargetNativeCompilation, + forTest: Boolean = false, + ) = + nativeLibraryBundler.addNativeLibrariesToAndroidVariantSources( + androidTarget = androidTarget, + nativeCompilation = nativeCompilation, + forTest = forTest, + provideSourceDirectories = { jniLibs }, + ) + + /** + * Convenience method to add bundle native libraries with a test jar. + * + * @see addNativeLibrariesToResources + */ + fun addNativeLibrariesToTestResources( + jvmTarget: KotlinJvmTarget, + nativeCompilation: MultiTargetNativeCompilation, + ) = + addNativeLibrariesToResources( + jvmTarget = jvmTarget, + nativeCompilation = nativeCompilation, + compilationName = KotlinCompilation.TEST_COMPILATION_NAME, + ) + + /** @see NativeLibraryBundler.addNativeLibrariesToResources */ + @JvmOverloads + fun addNativeLibrariesToResources( + jvmTarget: KotlinJvmTarget, + nativeCompilation: MultiTargetNativeCompilation, + compilationName: String = KotlinCompilation.MAIN_COMPILATION_NAME, + ) = + nativeLibraryBundler.addNativeLibrariesToResources( + jvmTarget = jvmTarget, + nativeCompilation = nativeCompilation, + compilationName = compilationName, + ) + + /** + * Sets the default target platform. + * + * The default target platform *must* be enabled in all build environments. For projects which + * request multiple target platforms, this method *must* be called to explicitly specify a + * default target platform. + * + * See [defaultPlatform] for details on how the value is used. + */ + fun defaultPlatform(value: PlatformIdentifier) { + defaultPlatform = value.id + } + + @JvmOverloads + fun jvm(block: Action? = null): KotlinJvmTarget? = potentiallyRedirecting("jvm") { + supportedPlatforms.add(PlatformIdentifier.JVM) + if (project.enableJvm()) { + kotlinExtension.jvm { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun jvmStubs( + runTests: Boolean = false, + block: Action? = null, + ): KotlinJvmTarget? { + supportedPlatforms.add(PlatformIdentifier.JVM_STUBS) + return if (project.enableJvm()) { + kotlinExtension.jvm("jvmStubs") { + block?.execute(this) + project.tasks.named("jvmStubsTest").configure { + // don't try running common tests for stubs target if disabled + it.enabled = runTests + } + } + } else { + null + } + } + + @JvmOverloads + fun androidNative(block: Action? = null): List { + return listOfNotNull( + androidNativeX86(block), + androidNativeX64(block), + androidNativeArm64(block), + androidNativeArm32(block), + ) + } + + @JvmOverloads + fun androidNativeX86(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("androidNativeX86") { + supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_X86) + if (project.enableAndroidNative()) { + kotlinExtension.androidNativeX86 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun androidNativeX64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("androidNativeX64") { + supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_X64) + if (project.enableAndroidNative()) { + kotlinExtension.androidNativeX64 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun androidNativeArm64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("androidNativeArm64") { + supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_ARM64) + if (project.enableAndroidNative()) { + kotlinExtension.androidNativeArm64 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun androidNativeArm32(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("androidNativeArm32") { + supportedPlatforms.add(PlatformIdentifier.ANDROID_NATIVE_ARM32) + if (project.enableAndroidNative()) { + kotlinExtension.androidNativeArm32 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun androidLibrary( + block: Action? = null + ): KotlinMultiplatformAndroidLibraryTarget? = potentiallyRedirecting("android") { + supportedPlatforms.add(PlatformIdentifier.ANDROID) + if (project.enableJvm()) { + agpKmpExtension.also { block?.execute(it) } + } else { + null + } + } + + @JvmOverloads + fun desktop(block: Action? = null): KotlinJvmTarget? = + potentiallyRedirecting("desktop") { + supportedPlatforms.add(PlatformIdentifier.DESKTOP) + if (project.enableDesktop()) { + kotlinExtension.jvm("desktop") { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun mingwX64(block: Action? = null): KotlinNativeTargetWithHostTests? = + potentiallyRedirecting("mingwX64") { + supportedPlatforms.add(PlatformIdentifier.MINGW_X_64) + if (project.enableWindows()) { + kotlinExtension.mingwX64 { block?.execute(this) } + } else { + null + } + } + + /** Configures all mac targets supported by AndroidX. */ + @JvmOverloads + fun mac(block: Action? = null): List { + return listOfNotNull(macosArm64(block)) + } + + @JvmOverloads + fun macosArm64(block: Action? = null): KotlinNativeTargetWithHostTests? = + potentiallyRedirecting("macosArm64") { + supportedPlatforms.add(PlatformIdentifier.MAC_ARM_64) + if (project.enableMac()) { + kotlinExtension.macosArm64 { block?.execute(this) } + } else { + null + } + } + + /** Configures all ios targets supported by AndroidX. */ + @JvmOverloads + fun ios(block: Action? = null): List { + return listOfNotNull(iosArm64(block), iosSimulatorArm64(block)) + } + + @JvmOverloads + fun iosArm64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("iosArm64") { + supportedPlatforms.add(PlatformIdentifier.IOS_ARM_64) + if (project.enableMac()) { + kotlinExtension.iosArm64 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun iosSimulatorArm64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("iosSimulatorArm64") { + supportedPlatforms.add(PlatformIdentifier.IOS_SIMULATOR_ARM_64) + if (project.enableMac()) { + kotlinExtension.iosSimulatorArm64 { block?.execute(this) } + } else { + null + } + } + + /** Configures all watchos targets supported by AndroidX. */ + @JvmOverloads + fun watchos(block: Action? = null): List { + return listOfNotNull( + watchosArm32(block), + watchosArm64(block), + // TODO(https://youtrack.jetbrains.com/issue/CMP-9513) publish it + // watchosDeviceArm64() + watchosSimulatorArm64(block), + ) + } + + @JvmOverloads + fun watchosArm32(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("watchosArm32") { + supportedPlatforms.add(PlatformIdentifier.WATCHOS_ARM_32) + if (project.enableMac()) { + kotlinExtension.watchosArm32 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun watchosArm64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("watchosArm64") { + supportedPlatforms.add(PlatformIdentifier.WATCHOS_ARM_64) + if (project.enableMac()) { + kotlinExtension.watchosArm64 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun watchosDeviceArm64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("watchosDeviceArm64") { + supportedPlatforms.add(PlatformIdentifier.WATCHOS_DEVICE_ARM_64) + if (project.enableMac()) { + kotlinExtension.watchosDeviceArm64 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun watchosSimulatorArm64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("watchosSimulatorArm64") { + supportedPlatforms.add(PlatformIdentifier.WATCHOS_SIMULATOR_ARM_64) + if (project.enableMac()) { + kotlinExtension.watchosSimulatorArm64 { block?.execute(this) } + } else { + null + } + } + + /** Configures all tvos targets supported by AndroidX. */ + @JvmOverloads + fun tvos(block: Action? = null): List { + return listOfNotNull(tvosArm64(block), tvosSimulatorArm64(block)) + } + + @JvmOverloads + fun tvosArm64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("tvosArm64") { + supportedPlatforms.add(PlatformIdentifier.TVOS_ARM_64) + if (project.enableMac()) { + kotlinExtension.tvosArm64 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun tvosSimulatorArm64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("tvosSimulatorArm64") { + supportedPlatforms.add(PlatformIdentifier.TVOS_SIMULATOR_ARM_64) + if (project.enableMac()) { + kotlinExtension.tvosSimulatorArm64 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun linux(block: Action? = null): List { + return listOfNotNull(linuxArm64(block), linuxX64(block)) + } + + @JvmOverloads + fun linuxArm64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("linuxArm64") { + supportedPlatforms.add(PlatformIdentifier.LINUX_ARM_64) + if (project.enableLinux()) { + kotlinExtension.linuxArm64 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun linuxX64(block: Action? = null): KotlinNativeTarget? = + potentiallyRedirecting("linuxX64") { + supportedPlatforms.add(PlatformIdentifier.LINUX_X_64) + if (project.enableLinux()) { + kotlinExtension.linuxX64 { block?.execute(this) } + } else { + null + } + } + + @JvmOverloads + fun linuxX64Stubs(block: Action? = null): KotlinNativeTarget? { + supportedPlatforms.add(PlatformIdentifier.LINUX_X_64_STUBS) + return if (project.enableLinux()) { + kotlinExtension.linuxX64("linuxx64Stubs") { + block?.execute(this) + project.tasks.named("linuxx64StubsTest").configure { + // don't try running common tests for stubs target + it.enabled = false + } + } + } else { + null + } + } + + @JvmOverloads + fun js(block: Action? = null): KotlinJsTargetDsl? = + potentiallyRedirecting("js") { + configureForkWebTarget( + platform = PlatformIdentifier.JS, + isEnabled = project.enableJs(), + createTarget = { configure -> kotlinExtension.js(configure) }, + block = block, + ) + } + + @OptIn(ExperimentalWasmDsl::class) + @JvmOverloads + fun wasmJs(block: Action? = null): KotlinWasmTargetDsl? = + potentiallyRedirecting("wasmJs") { + configureForkWebTarget( + platform = PlatformIdentifier.WASM_JS, + isEnabled = project.enableWasmJs(), + createTarget = { configure -> kotlinExtension.wasmJs(configure) }, + block = block, + ) + } + + // --- Artifact redirection (parallel-graph back-end): see `redirect { }` below. -------------- + + /** + * Redirect scope: inside `redirect("androidx.foo") { … }` the plain target functions + * (`androidLibrary {}`, `ios()`, `jvm()`, …) build their target **empty** and redirect it to the + * `androidx.*` artifact instead of compiling the real `commonMain` — the parallel-graph back-end + * publishes an empty klib/jar/aar that depends on the androidx coordinate. Mix freely with plain + * (fork-built) targets declared outside the block for partial redirects (e.g. + * `redirect("androidx.foo") { androidLibrary {} }` then plain `desktop(); ios()`). + * + * [group] is the target `androidx.*` group and is **required** — every redirect declares it + * explicitly (no property fallback, no derivation). [version] is optional: omit it to resolve + * from the `[versions]` table of `redirectversions.toml`; one redirect coordinate per module. + * + * The receiver is the decorated `androidXMultiplatform` extension itself (no separate scope + * object), so the target list is not duplicated and Groovy nested config closures (e.g. + * `androidLibrary { namespace = … }`) delegate to their target as usual. + */ + fun redirect(group: String, block: Action) = + redirect(group, null, block) + + fun redirect(group: String, version: String?, block: Action) { + val prevRedirectScope = redirectCoordinate + redirectCoordinate = RedirectCoordinate(group, version) + try { + block.execute(this) + } finally { + redirectCoordinate = prevRedirectScope + } + } + + /** + * Wraps a plain target function's creation. When called inside [redirect] { } the target's name + * is registered **before** the target (and its compilations) are created — so the + * default-hierarchy `excludeCompilations` predicate keeps the redirect leaf off the real + * `commonMain` — and the created target is recorded so the back-end re-roots it onto the empty + * `redirectCommonMain`. A no-op outside a redirect scope: the target is fork-built as usual. + * + * Every leaf target function (`jvm`, `androidLibrary`, `iosArm64`, …) routes its body through + * this helper, so any of them redirects automatically when invoked inside `redirect { }` — + * directly or via an aggregate like `ios()`/`mac()` that fans out to the leaves. + */ + private fun potentiallyRedirecting(targetName: String, create: () -> T): T { + val redirectScope = redirectCoordinate ?: return create() + expectRedirect(targetName) + return create().also { + (it as? KotlinTarget)?.let { target -> + recordRedirect(target, targetName, redirectScope) + } + } + } + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + private fun KotlinMultiplatformExtension.applyAndroidXDefaultHierarchyTemplate() = + applyDefaultHierarchyTemplate { + common { + // Artifact redirection: keep redirect targets (declared inside `redirect { }`) OUT of + // the common hierarchy entirely, so the template never wires them to `commonMain`. Their + // leaf source-sets are instead wired to the empty `redirectCommonMain` at + // target-creation time (see recordRedirect). This predicate is evaluated lazily per + // compilation, so the redirect set — populated by `potentiallyRedirecting` before the + // target is created — is already visible here. No-op for modules that declare no redirects. + excludeCompilations { it.target.name in redirectTargetNames } + group("jvmAndAndroid") { + // TODO(b/442950553): Switch to withAndroidTarget when bug is fixed + withCompilations { it is KotlinMultiplatformAndroidCompilation } + withJvm() + } + group("nonJvm") { + withNative() + group("web") { + withWasmJs() + withJs() + } + } + } + } + + private fun Project.configureWebTarget( + platform: PlatformIdentifier, + isEnabled: Boolean, + createTarget: (KotlinJsTargetDsl.() -> Unit) -> T, + block: Action? = null, + ): T? { + if (buildFeatures.isIsolatedProjectsEnabled()) return null + supportedPlatforms.add(platform) + return if (isEnabled) { + createTarget { + block?.execute(this) + binaries.library() + browser { + testTask { + it.useKarma { + useChromeHeadless() + useConfigDirectory(File(getSupportRootFolder(), "buildSrc/karmaconfig")) + } + } + } + // Do not place the config functions below before the browser DSL as the + // settings will be overridden + configureBinaryen() + configureDefaultIncrementalSyncTask() + configureKotlinJsTests() + configureNode() + + // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same: + // https://youtrack.jetbrains.com/issue/KT-71032 + configurePinnedKotlinLibraries(platform) + } + } else null + } + + /** Locates a project by path. */ + // This method is needed for Gradle project isolation to avoid calls to parent projects due to + // androidx { samples(project(":foo")) } + // Without this method, the call above results into a call to the parent object, because + // AndroidXExtension has `val project: Project`, which from groovy `project` call within + // `androidx` block tries retrieves that project object and calls to look for :foo property + // on it, then checking all the parents for it. + fun project(name: String): Project = project.project(name) + + companion object { + const val EXTENSION_NAME = "androidXMultiplatform" + } + + // FORK-only public extensions + + /** + * Configures native compilation tasks with flags to link required frameworks + */ + fun configureDarwinFlags() = org.jetbrains.androidx.build.configureDarwinFlags(project) + + /** + * Configure instrumented tests to run on an actual iOS simulator. + */ + fun iosInstrumentedTest() = org.jetbrains.androidx.build.addIosInstrumentedTestSourceset(project) +} + +// TODO(https://youtrack.jetbrains.com/issue/KT-76874/): +// Remove this function when the default destinationDirectory is different for each task +private fun Project.configureDefaultIncrementalSyncTask() { + val destinationPaths = + mapOf( + "jsDevelopmentLibraryCompileSync" to "js/packages/js/dev/kotlin", + "jsProductionLibraryCompileSync" to "js/packages/js/prod/kotlin", + "jsTestTestDevelopmentExecutableCompileSync" to "js/packages/js-test/dev/kotlin", + "jsTestTestProductionExecutableCompileSync" to "js/packages/js-test/prod/kotlin", + "wasmJsDevelopmentLibraryCompileSync" to "js/packages/wasm-js/dev/kotlin", + "wasmJsProductionLibraryCompileSync" to "js/packages/wasm-js/prod/kotlin", + "wasmJsTestTestDevelopmentExecutableCompileSync" to + "js/packages/wasm-js-test/dev/kotlin", + "wasmJsTestTestProductionExecutableCompileSync" to + "js/packages/wasm-js-test/prod/kotlin", + ) + + tasks.withType(DefaultIncrementalSyncTask::class.java).configureEach { task -> + val relativePath = + destinationPaths[task.name] + ?: throw IllegalArgumentException( + "No destination path configured for incremental‑sync task '${task.name}'" + ) + task.destinationDirectory.set(file(layout.buildDirectory.dir(relativePath))) + } +} + +internal fun Project.configureNode() { + val nodeJsPrebuilt = + File(project.getPrebuiltsRoot(), "androidx/external/org/nodejs/node").toURI().toString() + + plugins.withType().configureEach { + the().let { + it.version.set(getVersionByName("node")) + if (!ProjectLayoutType.isPlayground(this)) { + it.downloadBaseUrl.set(nodeJsPrebuilt) + } + } + } + plugins.withType().configureEach { + the().let { + it.version.set(getVersionByName("node")) + if (!ProjectLayoutType.isPlayground(this)) { + it.downloadBaseUrl.set(nodeJsPrebuilt) + } + } + } + + if (!ProjectLayoutType.isPlayground(this)) { + val javascriptPrebuiltsRoot = + File(project.getPrebuiltsRoot(), "androidx/javascript-for-kotlin") + + plugins.withType().configureEach { + the().let { + it.version.set(getVersionByName("yarn")) + it.yarnLockMismatchReport.set(YarnLockMismatchReport.FAIL) + it.downloadBaseUrl.set(javascriptPrebuiltsRoot.toURI().toString()) + } + } + + plugins.withType().configureEach { + the().let { + it.version.set(getVersionByName("yarn")) + it.yarnLockMismatchReport.set(YarnLockMismatchReport.FAIL) + it.downloadBaseUrl.set(javascriptPrebuiltsRoot.toURI().toString()) + } + } + } +} + +@OptIn(ExperimentalWasmDsl::class) +private fun Project.configureBinaryen() { + if (ProjectLayoutType.isPlayground(project)) { + return + } + plugins.withType().configureEach { + the() + .downloadBaseUrl + .set( + File(project.getPrebuiltsRoot(), "androidx/javascript-for-kotlin/binaryen") + .toURI() + .toString() + ) + } +} + +internal fun Project.configurePinnedKotlinLibraries(platform: PlatformIdentifier) { + multiplatformExtension?.let { + val kotlinLibSuffix = + when (platform) { + PlatformIdentifier.JS -> "js" + PlatformIdentifier.WASM_JS -> "wasm-js" + else -> throw IllegalStateException("Unsupported platform: $platform") + } + val kotlinVersion = project.getVersionByName("kotlin") + it.sourceSets.getByName("${platform.id}Main").dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-$kotlinLibSuffix:$kotlinVersion") + } + it.sourceSets.getByName("${platform.id}Test").dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-$kotlinLibSuffix:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-test-$kotlinLibSuffix:$kotlinVersion") + } + } +} + +private fun Project.configureKotlinJsTests() { + tasks.withType(KotlinJsTest::class.java).configureEach { task -> + if (!ProjectLayoutType.isPlayground(this)) { + val unzipChromeBuildServiceProvider = + gradle.sharedServices.registrations.getByName("unzipChrome").service + task.usesService(unzipChromeBuildServiceProvider) + // Remove doFirst and switch to FileProperty property to set browser path when issue + // https://youtrack.jetbrains.com/issue/KT-72514 is resolved + task.doFirst { + task.environment( + "CHROME_BIN", + (unzipChromeBuildServiceProvider.get() as UnzipChromeBuildService).chromePath, + ) + } + } + // From: https://nodejs.org/api/cli.html + task.nodeJsArgs.addAll(listOf("--trace-warnings", "--trace-uncaught", "--trace-sigint")) + } + + // Compiler Arg needed for tests only: https://youtrack.jetbrains.com/issue/KT-59081 + tasks.withType(Kotlin2JsCompile::class.java).configureEach { task -> + if (task.name.lowercase().contains("test")) { + task.compilerOptions.freeCompilerArgs.add("-Xwasm-enable-array-range-checks") + } + } +} + +fun Project.validatePublishedMultiplatformHasDefault() { + val extension = project.extensions.getByType(AndroidXMultiplatformExtension::class.java) + if (extension.defaultPlatform == null && extension.supportedPlatforms.isNotEmpty()) { + throw GradleException( + "Project is published and multiple platforms are requested. You " + + "must explicitly specify androidXMultiplatform.defaultPlatform as one of: " + + extension.targetPlatforms.joinToString(", ") { + "PlatformIdentifier.${PlatformIdentifier.fromId(it)!!.name}" + } + ) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt new file mode 100644 index 0000000000000..5f4e061952000 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXPlaygroundRootImplPlugin.kt @@ -0,0 +1,242 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.gradle.extraPropertyOrNull +import androidx.build.gradle.isRoot +import groovy.xml.DOMBuilder +import java.net.URI +import java.net.URL +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.api.tasks.testing.AbstractTestTask +import org.gradle.work.DisableCachingByDefault + +/** + * This plugin is used in Playground projects and adds functionality like resolving to snapshot + * artifacts instead of projects or allowing access to public maven repositories. + */ +@Suppress("unused") // used in Playground Projects +class AndroidXPlaygroundRootImplPlugin : Plugin { + private lateinit var rootProject: Project + + /** List of snapshot repositories to fetch AndroidX artifacts */ + private lateinit var repos: PlaygroundRepositories + + /** The configuration for the plugin read from the gradle properties */ + private lateinit var config: PlaygroundProperties + + /** List of projects that were requested in the settings.gradle file */ + private lateinit var primaryProjectPaths: Set + + override fun apply(target: Project) { + if (!target.isRoot) { + throw GradleException("This plugin should only be applied to root project") + } + if (!target.plugins.hasPlugin(AndroidXRootImplPlugin::class.java)) { + throw GradleException( + "Must apply AndroidXRootImplPlugin before applying AndroidXPlaygroundRootImplPlugin" + ) + } + rootProject = target + config = PlaygroundProperties.load(rootProject) + repos = PlaygroundRepositories(config) + rootProject.repositories.addPlaygroundRepositories() + GradleTransformWorkaround.maybeApply(rootProject) + PlaygroundCIHostTestsTask.register(rootProject) + primaryProjectPaths = + target.extensions.extraProperties.get("primaryProjects")!!.toString().split(",").toSet() + rootProject.subprojects { configureSubProject(it) } + } + + private fun configureSubProject(project: Project) { + project.repositories.addPlaygroundRepositories() + project.configurations.configureEach { configuration -> + configuration.resolutionStrategy.eachDependency { details -> + val requested = details.requested + if (requested.version == SNAPSHOT_MARKER) { + val snapshotVersion = findSnapshotVersion(requested.group, requested.name) + details.useVersion(snapshotVersion) + } + } + } + if (project.path in primaryProjectPaths) { + project.tasks.withType(AbstractTestTask::class.java).configureEach { + PlaygroundCIHostTestsTask.addTask(project, it) + } + } + } + + /** + * Finds the snapshot version from the AndroidX snapshot repository. + * + * This is initially done by reading the maven-metadata from the snapshot repository. The result + * of that query is cached in the build file so that subsequent build requests will not need to + * access the network. + */ + private fun findSnapshotVersion(group: String, module: String): String { + @Suppress("DEPRECATION") + val snapshotVersionCache = + rootProject.buildDir.resolve("snapshot-version-cache/${config.snapshotBuildId}") + val groupPath = group.replace('.', '/') + val modulePath = module.replace('.', '/') + val metadataCacheFile = snapshotVersionCache.resolve("$groupPath/$modulePath/version.txt") + return if (metadataCacheFile.exists()) { + metadataCacheFile.readText(Charsets.UTF_8) + } else { + val metadataUrl = "${repos.snapshots.url}/$groupPath/$modulePath/maven-metadata.xml" + @Suppress("deprecation") + URL(metadataUrl).openStream().use { + val parsedMetadata = DOMBuilder.parse(it.reader()) + val versionNodes = parsedMetadata.getElementsByTagName("latest") + if (versionNodes.length != 1) { + throw GradleException( + "AndroidXPlaygroundRootImplPlugin#findSnapshotVersion expected exactly " + + " one latest version in $metadataUrl, but got ${versionNodes.length}" + ) + } + val snapshotVersion = versionNodes.item(0).textContent + metadataCacheFile.parentFile.mkdirs() + metadataCacheFile.writeText(snapshotVersion, Charsets.UTF_8) + snapshotVersion + } + } + } + + private fun RepositoryHandler.addPlaygroundRepositories() { + repos.all.forEach { playgroundRepository -> + maven { repository -> + repository.url = URI(playgroundRepository.url) + repository.metadataSources { + it.mavenPom() + it.artifact() + } + repository.content { + it.includeGroupByRegex(playgroundRepository.includeGroupRegex) + if (playgroundRepository.includeModuleRegex != null) { + it.includeModuleByRegex( + playgroundRepository.includeGroupRegex, + playgroundRepository.includeModuleRegex, + ) + } + } + } + } + google { repository -> + repository.content { + it.includeGroupByRegex("androidx.*") + it.includeGroupByRegex("com\\.android.*") + it.includeGroupByRegex("com\\.google.*") + } + } + mavenCentral() + gradlePluginPortal() + } + + private class PlaygroundRepositories(props: PlaygroundProperties) { + val snapshots = + PlaygroundRepository( + "https://androidx.dev/snapshots/builds/${props.snapshotBuildId}/artifacts" + + "/repository", + includeGroupRegex = """androidx\..*""", + ) + val metalava = + PlaygroundRepository( + "https://androidx.dev/metalava/builds/${props.metalavaBuildId}/artifacts" + + "/repo/m2repository", + includeGroupRegex = """com\.android\.tools\.metalava""", + ) + val prebuilts = + PlaygroundRepository( + INTERNAL_PREBUILTS_REPO_URL, + includeGroupRegex = """androidx\..*""", + ) + val dokka = + PlaygroundRepository( + "https://packages.jetbrains.team/maven/p/kt/dokka-dev", + includeGroupRegex = """org\.jetbrains\.dokka""", + ) + val kotlinDev = + PlaygroundRepository( + "https://packages.jetbrains.team/maven/p/kt/dev/", + includeGroupRegex = """org\.jetbrains\.kotlin.*""", + ) + val mavenSnapshots = + PlaygroundRepository( + "https://central.sonatype.com/repository/maven-snapshots/", + includeGroupRegex = """com\.google\.devtools.*""", + ) + val all = listOf(snapshots, metalava, dokka, prebuilts, kotlinDev, mavenSnapshots) + } + + private data class PlaygroundRepository( + val url: String, + val includeGroupRegex: String, + val includeModuleRegex: String? = null, + ) + + private data class PlaygroundProperties( + val snapshotBuildId: String, + val metalavaBuildId: String, + ) { + companion object { + fun load(project: Project): PlaygroundProperties { + return PlaygroundProperties( + snapshotBuildId = project.requireProperty(PLAYGROUND_SNAPSHOT_BUILD_ID), + metalavaBuildId = project.requireProperty(PLAYGROUND_METALAVA_BUILD_ID), + ) + } + + private fun Project.requireProperty(name: String): String { + return checkNotNull(extraPropertyOrNull(name)) { + "missing $name property. It must be defined in the gradle.properties file" + } + .toString() + } + } + } + + companion object { + const val INTERNAL_PREBUILTS_REPO_URL = + "https://androidx.dev/storage/prebuilts/androidx/internal/repository" + } + + @DisableCachingByDefault(because = "This is an anchor task that does no work.") + abstract class PlaygroundCIHostTestsTask : DefaultTask() { + init { + group = "Verification" + description = + "Runs host tests that belong to the projects which were explicitly " + + "requested in the playground setup." + } + + companion object { + private const val NAME = "playgroundCIHostTests" + + fun addTask(project: Project, task: AbstractTestTask) { + project.rootProject.tasks.named(NAME).configure { it.dependsOn(task) } + } + + fun register(project: Project) { + project.tasks.register(NAME, PlaygroundCIHostTestsTask::class.java) + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRepackageImplPlugin.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRepackageImplPlugin.kt new file mode 100644 index 0000000000000..2a0ee5429951b --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRepackageImplPlugin.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import groovy.lang.Closure +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaLibraryPlugin +import org.gradle.api.provider.Property +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.TaskProvider +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.create +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin + +/** + * Plugin responsible for repackaging libraries. The plugin repackages what is set in the + * [RelocationExtension] by the user and reconfigures the JAR task to output the repackaged classes + * JAR. + */ +@Suppress("unused") +class AndroidXRepackageImplPlugin : Plugin { + + override fun apply(project: Project) { + val relocationExtension = + project.extensions.create(EXTENSION_NAME, project) + project.plugins.configureEach { plugin -> + when (plugin) { + is JavaLibraryPlugin, + is KotlinBasePlugin -> project.configureJavaOrKotlinLibrary(relocationExtension) + } + } + } + + private fun Project.configureJavaOrKotlinLibrary(relocationExtension: RelocationExtension) { + createConfigurations() + + val sourceSets = extensions.getByType(SourceSetContainer::class.java) + val libraryShadowJar = + tasks.register("shadowLibraryJar", ShadowJar::class.java) { task -> + task.transformers.add( + BundleInsideHelper.DontIncludeResourceTransformer().apply { + dropResourcesWithSuffix = ".proto" + } + ) + task.transformers.add( + BundleInsideHelper.DontIncludeResourceTransformer().apply { + dropResourcesWithSuffix = ".proto.bin" + } + ) + task.from(sourceSets.named("main").map { it.output }) + relocationExtension.getRelocations().forEach { + task.relocate(it.sourcePackage, it.targetPackage) + } + relocationExtension.artifactId.orNull?.let { + task.configurations = listOf(configurations.getByName("repackageClasspath")) + } + } + addArchiveToVariants(libraryShadowJar) + } + + private fun Project.createConfigurations() { + val repackage = + configurations.register("repackage") { config -> + config.isCanBeConsumed = false + config.isCanBeResolved = false + } + + configurations.register("repackageClasspath") { config -> + config.isCanBeConsumed = false + config.isCanBeResolved = true + // remove .get() when https://github.com/gradle/gradle/issues/33396 is fixed + config.extendsFrom(repackage.get()) + } + + tasks.named("jar", Jar::class.java) { + // We cannot have two tasks with the same output as the ListTaskOutputsTask will fail. + // As we want the repackaged jar as the published artifact, we change the + // name of classifier of the JAR task + it.archiveClassifier.set("before-shadow") + } + + forceJarUsageForAndroid() + } + + /** + * This forces the use of repackaged JARs as opposed to the java-classes-directory for Android. + * Without this, AGP uses the artifacts in java-classes-directory, which do not have the classes + * repackaged to the target package. + * + * We attempted to extract the contents of the repackaged library JAR into classes/java/main, + * but the AGP transform depends on JavaCompile. We cannot make JavaCompile depend on the task + * that creates the shadowed library as that would result in a circular dependency. + */ + private fun Project.forceJarUsageForAndroid() = + configurations.configureEach { configuration -> + if (configuration.name == "runtimeElements") { + configuration.outgoing.variants.removeIf { it.name == "classes" } + } + } + + private fun Project.addArchiveToVariants(task: TaskProvider) = + configurations.configureEach { configuration -> + if (configuration.name == "apiElements" || configuration.name == "runtimeElements") { + configuration.outgoing.artifacts.clear() + configuration.outgoing.artifact(task) + } + } + + companion object { + const val EXTENSION_NAME = "repackage" + } +} + +class Relocation { + /* The package name and any import statements for a class that are to be relocated. */ + var sourcePackage: String? = null + + /* The package name and any import statements for a class to which they should be relocated. */ + var targetPackage: String? = null +} + +abstract class RelocationExtension(val project: Project) { + + private var relocations: MutableCollection = ArrayList() + + fun addRelocation(closure: Closure): Relocation { + val relocation = project.configure(Relocation(), closure) as Relocation + relocations.add(relocation) + return relocation + } + + fun getRelocations(): Collection { + return relocations + } + + /* Optional artifact id if the user wants to publish the dependency in the shadowed config. */ + abstract val artifactId: Property +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt new file mode 100644 index 0000000000000..7e8ec94310dab --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt @@ -0,0 +1,250 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.AndroidXImplPlugin.Companion.FINALIZE_TEST_CONFIGS_WITH_APKS_TASK +import androidx.build.AndroidXImplPlugin.Companion.ZIP_TEST_CONFIGS_WITH_APKS_TASK +import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask +import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask.Companion.CREATE_AGGREGATE_BUILD_INFO_FILES_TASK +import androidx.build.dependencyTracker.AffectedModuleDetector +import androidx.build.gradle.isRoot +import androidx.build.license.ValidateLicensesExistTask +import androidx.build.logging.TERMINAL_RED +import androidx.build.logging.TERMINAL_RESET +import androidx.build.playground.ValidateIntegrationPatches +import androidx.build.playground.VerifyPlaygroundGradleConfigurationTask +import androidx.build.studio.StudioTask.Companion.registerStudioTask +import androidx.build.testConfiguration.registerOwnersServiceTasks +import androidx.build.uptodatedness.TaskUpToDateValidator +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION +import java.io.File +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.configuration.BuildFeatures +import org.gradle.api.file.RelativePath +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.bundling.Zip +import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.gradle.build.event.BuildEventsListenerRegistry +import org.gradle.kotlin.dsl.extra +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask +import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinToolingSetupTask + +abstract class AndroidXRootImplPlugin : Plugin { + @get:Inject abstract val registry: BuildEventsListenerRegistry + @get:Inject abstract val buildFeatures: BuildFeatures + + override fun apply(project: Project) { + if (!project.isRoot) { + throw Exception("This plugin should only be applied to root project") + } + project.configureRootProject() + } + + private fun Project.configureRootProject() { + project.validateAllAndroidxArgumentsAreRecognized() + tasks.register("listAndroidXProperties", ListAndroidXPropertiesTask::class.java) + tasks.register("createProject", ProjectCreatorTask::class.java) + configureKtfmtCheckFile() + maybeRegisterFilterableTask() + registerListAffectedProjectsTask() + + /* In JetBrains Fork we don't force AGP version. + // If we're running inside Studio, validate the Android Gradle Plugin version. + val expectedAgpVersion = System.getenv("EXPECTED_AGP_VERSION") + if (providers.gradleProperty("android.injected.invoked.from.ide").isPresent) { + if (expectedAgpVersion != ANDROID_GRADLE_PLUGIN_VERSION) { + throw GradleException( + """ + Please close and restart Android Studio. + + Expected AGP version \"$expectedAgpVersion\" does not match actual AGP version + \"$ANDROID_GRADLE_PLUGIN_VERSION\". This happens when AGP is updated while + Studio is running and can be fixed by restarting Studio. + """ + .trimIndent() + ) + } + } + */ + + val verifyPlayground = VerifyPlaygroundGradleConfigurationTask.createIfNecessary(project) + + val aggregateBuildInfo = + if (!buildFeatures.isIsolatedProjectsEnabled()) { + tasks.register( + CREATE_AGGREGATE_BUILD_INFO_FILES_TASK, + CreateAggregateLibraryBuildInfoFileTask::class.java, + ) + } else null + + val attestationManifest = + if (!buildFeatures.isIsolatedProjectsEnabled()) { + tasks.register(ATTESTATION_TASK_NAME, AttestationManifestTask::class.java) { task -> + task.manifestFile.set( + getDistributionDirectory().file("attestation_manifest.json") + ) + } + } else null + tasks.register(BUILD_ON_SERVER_TASK, BuildOnServerTask::class.java) { task -> + task.cacheEvenIfNoOutputs() + task.aggregateBuildInfoFile.set( + getDistributionDirectory().file(AGGREGATE_BUILD_INFO_FILE_NAME) + ) + verifyPlayground?.let { task.dependsOn(it) } + aggregateBuildInfo?.let { task.dependsOn(it) } + attestationManifest?.let { task.dependsOn(it) } + } + + extra.set("projects", ConcurrentHashMap()) + + /** + * Copy App APKs (from ApkOutputProviders) into [getTestConfigDirectory] before zipping. + * Flatten directory hierarchy as both TradeFed and FTL work with flat hierarchy. + */ + val finalizeConfigsTask = + project.tasks.register(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK, Copy::class.java) { + it.from(project.getAppApksFilesDirectory()) + it.into(project.getTestConfigDirectory()) + it.eachFile { f -> f.relativePath = RelativePath(true, f.name) } + it.includeEmptyDirs = false + } + + // NOTE: this task is used by the Github CI as well. If you make any changes here, + // please update the .github/workflows files as well, if necessary. + project.tasks.register(ZIP_TEST_CONFIGS_WITH_APKS_TASK, Zip::class.java) { + // Flatten PrivacySandbox APKs in separate task to preserve file order in resulting ZIP. + it.dependsOn(finalizeConfigsTask) + it.destinationDirectory.set(project.getDistributionDirectory()) + it.archiveFileName.set("androidTest.zip") + it.from(project.getTestConfigDirectory()) + // We're mostly zipping a bunch of .apk files that are already compressed + it.entryCompression = ZipEntryCompression.STORED + // Archive is greater than 4Gb :O + it.isZip64 = true + it.isReproducibleFileOrder = true + } + + AffectedModuleDetector.configure(gradle, this) + + if (!buildFeatures.isIsolatedProjectsEnabled()) { + registerOwnersServiceTasks() + } + registerStudioTask() + + project.tasks.register("listTaskOutputs", ListTaskOutputsTask::class.java) { task -> + task.outputFile.set(project.getDistributionDirectory().file("task_outputs.txt")) + task.removePrefix(project.getCheckoutRoot().path) + } + + TaskUpToDateValidator.setup(project, registry) + + /** + * Add dependency analysis plugin and add buildHealth task to buildOnServer when + * maxDepVersions is not enabled + */ + if (!project.usingMaxDepVersions().get()) { + project.plugins.apply("com.autonomousapps.dependency-analysis") + + // Ignore advice regarding ktx dependencies + val dependencyAnalysis = + project.extensions.getByType( + com.autonomousapps.DependencyAnalysisExtension::class.java + ) + dependencyAnalysis.structure { it.ignoreKtx(true) } + } + project.configureTasksForKotlinWeb() + + tasks.register("checkExternalLicenses", ValidateLicensesExistTask::class.java) { + it.prebuiltsDirectory.set(File(getPrebuiltsRoot(), "androidx/external")) + it.baseline.set(layout.projectDirectory.file("license-baseline.txt")) + it.cacheEvenIfNoOutputs() + } + + ValidateIntegrationPatches.createTask(project) + + fetchDevelocityKeysIfNeeded() + } + + private fun Project.configureTasksForKotlinWeb() { + val offlineMirrorStorage = + if (ProjectLayoutType.isPlayground(this)) { + project.file( + layout.buildDirectory.dir("javascript-for-playground").map { + it.asFile.also { file -> file.mkdirs() } + } + ) + } else { + File(getPrebuiltsRoot(), "androidx/javascript-for-kotlin") + } + + val createYarnRcFileTask = + tasks.register("createYarnRcFile", CreateYarnRcFileTask::class.java) { + it.offlineMirrorStorage.set(offlineMirrorStorage) + it.cacheStorage.set(layout.buildDirectory.dir("yarnCache")) + it.yarnrcFile.set(layout.buildDirectory.file(".yarnrc")) + } + val createWasmYarnRcFileTask = + tasks.register("createWasmYarnRcFile", CreateYarnRcFileTask::class.java) { + it.offlineMirrorStorage.set(offlineMirrorStorage) + it.cacheStorage.set(layout.buildDirectory.dir("wasmYarnCache")) + it.yarnrcFile.set(layout.buildDirectory.file("wasm/.yarnrc")) + } + + configureNode() + + // ensure yarn install is complete before using it to install kotlin wasm tooling + tasks.withType().configureEach { + it.dependsOn(tasks.withType()) + } + + tasks.withType().configureEach { + when (it.name) { + "kotlinNpmInstall" -> it.dependsOn(createYarnRcFileTask) + "kotlinWasmNpmInstall" -> it.dependsOn(createWasmYarnRcFileTask) + } + it.args.addAll(listOf("--ignore-engines", "--verbose")) + if (project.useYarnOffline()) { + it.args.add("--offline") + it.additionalFiles.plus(offlineMirrorStorage) + it.doFirst { + println( + """ + Fetching yarn packages from the offline mirror: ${offlineMirrorStorage.path}. + Your build will fail if a package is not in the offline mirror. To fix, run: + + $TERMINAL_RED./gradlew kotlinNpmInstall kotlinWasmNpmInstall -Pandroidx.yarnOfflineMode=false && ./gradlew kotlinUpgradeYarnLock kotlinWasmUpgradeYarnLock$TERMINAL_RESET + + this will download the dependencies from the internet and update the lockfile. + Don't forget to upload the changes to Gerrit! + """ + .trimIndent() + .replace("\n", " ") + ) + } + } + } + } +} + +internal const val AGGREGATE_BUILD_INFO_FILE_NAME = "androidx_aggregate_build_info.txt" diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AttestationManifestTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AttestationManifestTask.kt new file mode 100644 index 0000000000000..db94dca8cf8f1 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/AttestationManifestTask.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.named + +@CacheableTask +abstract class AttestationManifestTask : DefaultTask() { + @get:Input abstract val sbomMap: MapProperty + + @get:Input abstract val zipMap: MapProperty + + @get:OutputFile abstract val manifestFile: RegularFileProperty + + @TaskAction + fun writeManifest() { + val output = + zipMap.get().keys.joinToString(separator = ",\n", prefix = "[\n", postfix = "\n]") { key + -> + check(sbomMap.get().containsKey(key)) { + "sbomMap is missing an entry for $key project" + } + """ { + "artifact_path": "${zipMap.get()[key]!!}", + "sbom_path": "${sbomMap.get()[key]!!}", + "attest_archive_contents": true + }""" + } + manifestFile.get().asFile.writeText(output) + } +} + +internal fun Project.addSbomToAttestation(relativeSbomPath: Provider) { + rootProject.tasks.named(ATTESTATION_TASK_NAME).configure { manifestTask + -> + manifestTask.sbomMap.put(path, relativeSbomPath) + } +} + +internal fun Project.addZipToAttestation(relativeZipPath: Provider) { + if (ProjectLayoutType.isPlayground(this)) return + rootProject.tasks.named(ATTESTATION_TASK_NAME).configure { manifestTask + -> + manifestTask.zipMap.put(path, relativeZipPath) + } +} + +internal const val ATTESTATION_TASK_NAME = "attestationManifest" diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/BenchmarkConfiguration.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/BenchmarkConfiguration.kt new file mode 100644 index 0000000000000..89725cdb00a7d --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/BenchmarkConfiguration.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import com.android.build.api.variant.HasDeviceTests +import org.gradle.api.Project + +/** + * Enable internal defaults for microbenchmark which can be used to set defaults we aren't ready to + * apply publicly, or which require root to function. + * + * See [androidx.build.testConfiguration.INST_ARG_BLOCKLIST], which can be used to suppress some of + * these args in CI. + */ +internal fun HasDeviceTests.enableMicrobenchmarkInternalDefaults(project: Project) { + if (project.hasBenchmarkPlugin()) { + deviceTests.forEach { (_, deviceTest) -> + // Enables CPU perf event counters both locally, and in CI + deviceTest.instrumentationRunnerArguments.put( + "androidx.benchmark.cpuEventCounter.enable", + "true", + ) + + // Set default events to aid in CI investigations of run to run noise + // Avoid using more than three, or capture may fail reporting all zeros, see b/291826415 + deviceTest.instrumentationRunnerArguments.put( + "androidx.benchmark.cpuEventCounter.events", + "Instructions,L1DMisses,BranchMisses", + ) + + // Force AndroidX devs to disable JIT on rooted devices + deviceTest.instrumentationRunnerArguments.put( + "androidx.benchmark.requireJitDisabledIfRooted", + "true", + ) + + // Check that speed compilation always used when benchmark invoked + deviceTest.instrumentationRunnerArguments.put("androidx.benchmark.requireAot", "true") + + // Throw if measureRepeated() called on main thread to avoid ANRs + deviceTest.instrumentationRunnerArguments.put( + "androidx.benchmark.throwOnMainThreadMeasureRepeated", + "true", + ) + + // Enables long-running method tracing on the UI thread, even if that risks ANR for + // profiling convenience. + // NOTE, this *must* be suppressed in CI!! + deviceTest.instrumentationRunnerArguments.put( + "androidx.benchmark.profiling.skipWhenDurationRisksAnr", + "false", + ) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/BuildOnServerTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/BuildOnServerTask.kt new file mode 100644 index 0000000000000..e6d7376ff1946 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/BuildOnServerTask.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.io.FileNotFoundException +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** + * Task for building all of Androidx libraries and documentation + * + * AndroidXImplPlugin configuration adds dependencies to BuildOnServer for all of the tasks that + * produce artifacts that we want to build on server builds When BuildOnServer executes, it + * double-checks that all expected artifacts were built + */ +@CacheableTask +abstract class BuildOnServerTask : DefaultTask() { + + init { + group = "Build" + description = "Builds all of the Androidx libraries and documentation" + } + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val aggregateBuildInfoFile: RegularFileProperty + + @TaskAction + fun checkAllBuildOutputs() { + if (!aggregateBuildInfoFile.get().asFile.exists()) { + throw FileNotFoundException( + "buildOnServer required output missing: " + + "${aggregateBuildInfoFile.get().asFile.path}" + ) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/CheckKotlinApiTargetTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/CheckKotlinApiTargetTask.kt new file mode 100644 index 0000000000000..23e117df41af1 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/CheckKotlinApiTargetTask.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.build + +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + +/** Check if the kotlin-stdlib transitive dependencies are the same as the project specified one. */ +@DisableCachingByDefault(because = "not worth caching") +abstract class CheckKotlinApiTargetTask : DefaultTask() { + + @get:Input abstract val kotlinTarget: Property + + @get:Internal val projectPath: String = project.path + + @get:Input + val allDependencies: Provider>> = + project.provider { + project.configurations + .filter(project::shouldVerifyConfiguration) + .filter { it.isCanBeResolved && it.isPublished() } + .flatMap { config -> + config.incoming.resolutionResult.allComponents.mapNotNull { component -> + (component.id as? ModuleComponentIdentifier)?.let { id -> + "${id.module}:${id.version}" to config.name + } + } + } + } + + @get:OutputFile abstract val outputFile: RegularFileProperty + + @TaskAction + fun check() { + val incompatibleConfigurations = + allDependencies + .get() + .asSequence() + .filter { it.first.startsWith("kotlin-stdlib:") } + .map { it.first.substringAfter(":") to it.second } + .map { KotlinVersion.fromVersion(it.first.substringBeforeLast('.')) to it.second } + .filter { it.first > kotlinTarget.get() } + .map { "${it.second} (${it.first})" } + .toList() + + val outputFile = outputFile.get().asFile + outputFile.parentFile.mkdirs() + + if (incompatibleConfigurations.isNotEmpty()) { + val errorMessage = + incompatibleConfigurations.joinToString( + separator = "\n - ", + prefix = + "The project's kotlin-stdlib target is ${kotlinTarget.get()} but these " + + "configurations are pulling in higher versions of kotlin-stdlib:\n - ", + postfix = + "\n\nRun ./gradlew $projectPath:dependencies to see which dependency is " + + "pulling in the incompatible kotlin-stdlib", + ) + outputFile.writeText("FAILURE: $errorMessage") + throw IllegalStateException(errorMessage) + } + } + + companion object { + const val TASK_NAME = "checkKotlinApiTarget" + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ClasspathBuilder.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ClasspathBuilder.kt new file mode 100644 index 0000000000000..3d9c86d000465 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ClasspathBuilder.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Project +import org.gradle.api.file.FileCollection + +/** + * Returns a FileCollection that is a classpath of the library defined in the libs.versions.toml. + */ +fun Project.getLibraryClasspath(libraryName: String): FileCollection { + return configurations + .detachedConfiguration(dependencies.create(getLibraryByName(libraryName))) + .incoming + .files +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ConfigureAarAsJar.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ConfigureAarAsJar.kt new file mode 100644 index 0000000000000..09984dcf20739 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ConfigureAarAsJar.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import com.android.build.api.attributes.BuildTypeAttr +import org.gradle.api.Project +import org.gradle.api.artifacts.type.ArtifactTypeDefinition +import org.gradle.api.attributes.Usage +import org.gradle.api.attributes.java.TargetJvmEnvironment + +/** + * Creates `[configurationName]AarAsJar` config for JVM tests that need Android library classes on + * the classpath. + */ +internal fun configureAarAsJarForConfiguration(project: Project, configurationName: String) { + val releaseVariant = + project.objects.named(BuildTypeAttr::class.java, Release.DEFAULT_PUBLISH_CONFIG) + val javaApiUsage = project.objects.named(Usage::class.java, Usage.JAVA_API) + val androidJvmEnv = + project.objects.named(TargetJvmEnvironment::class.java, TargetJvmEnvironment.ANDROID) + + val aarAsJarConfig = + project.configurations.register("${configurationName}AarAsJar") { + it.isTransitive = false + it.isCanBeConsumed = false + it.isCanBeResolved = true + + it.attributes.apply { + attribute(BuildTypeAttr.ATTRIBUTE, releaseVariant) + attribute(Usage.USAGE_ATTRIBUTE, javaApiUsage) + attribute(TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, androidJvmEnv) + attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, "android-classes-jar") + } + } + + project.configurations.named(configurationName) { config -> + config.dependencies.add(project.dependencies.create(aarAsJarConfig.get().incoming.files)) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/CreateYarnRcTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/CreateYarnRcTask.kt new file mode 100644 index 0000000000000..e9e96e6a359a9 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/CreateYarnRcTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** + * Creates an `.yarnrc` file in a specified directory. The `.yarnrc` file will contain the path to + * the offline storage of the required dependencies. + */ +@DisableCachingByDefault(because = "not worth caching") +abstract class CreateYarnRcFileTask : DefaultTask() { + + @get:InputDirectory + @get:PathSensitive(PathSensitivity.ABSOLUTE) + abstract val offlineMirrorStorage: DirectoryProperty + + @get:OutputDirectory abstract val cacheStorage: DirectoryProperty + + @get:OutputFile abstract val yarnrcFile: RegularFileProperty + + @TaskAction + fun createFile() { + val offlineStoragePath = offlineMirrorStorage.get().asFile.absolutePath + val cacheStoragePath = cacheStorage.get().asFile.absolutePath + yarnrcFile.get().asFile.let { + it.parentFile.mkdirs() + it.writeText( + """ + yarn-offline-mirror "$offlineStoragePath" + cache-folder "$cacheStoragePath" + """ + .trimIndent() + ) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/DependencyAnalysisPostProcessingTasks.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/DependencyAnalysisPostProcessingTasks.kt new file mode 100644 index 0000000000000..2deb4e5785381 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/DependencyAnalysisPostProcessingTasks.kt @@ -0,0 +1,282 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.logging.TERMINAL_RED +import androidx.build.logging.TERMINAL_RESET +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import com.autonomousapps.AbstractPostProcessingTask +import com.autonomousapps.model.ModuleCoordinates +import com.autonomousapps.model.ProjectAdvice +import com.autonomousapps.model.ProjectCoordinates +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import java.io.File +import kotlin.text.appendLine +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** + * Task that reports dependency analysis advice for the project. It gets advice from the dependency + * analysis gradle plugin and checks the baselines for the advice already captured and only reports + * if additional violations are found. + */ +@CacheableTask +abstract class ReportDependencyAnalysisAdviceTask : AbstractPostProcessingTask() { + init { + group = "Verification" + description = "Task for generating advice for dependency analysis" + } + + @get:Internal abstract val baseLineFile: RegularFileProperty + + @InputFile + @Optional + @PathSensitive(PathSensitivity.NONE) + fun getDependencyAnalysisBaseline(): File? = baseLineFile.get().asFile.takeIf { it.exists() } + + @get:Internal val projectPath: String = project.path + @get:Internal val isKMP: Boolean = project.multiplatformExtension != null + @get:Internal + val isPublishedLibrary: Boolean = + project.extensions.getByType(AndroidXExtension::class.java).type == + SoftwareType.PUBLISHED_LIBRARY + + @TaskAction + fun getAdvice() { + val projectAdvice = + this@ReportDependencyAnalysisAdviceTask.projectAdvice().toAndroidxProjectAdvice() + + val baselineAdvice = + Gson() + .fromJson( + getDependencyAnalysisBaseline()?.readText(), + AndroidxProjectAdvice::class.java, + ) + + val advice = + if (baselineAdvice != null) { + getIncrementalAdvice( + projectAdvice.dependencyAdvice.filter { + !baselineAdvice.dependencyAdvice.contains(it) + } + ) + } else { + getIncrementalAdvice(projectAdvice.dependencyAdvice) + } + + if (advice.isNotBlank()) { + error( + """ + There are some new dependencies added to this change that might be misconfigured: + $advice + ******************************************************************************** + $TERMINAL_RED + To get a complete list of misconfigured dependencies, please run: + ./gradlew $projectPath:projectHealth. + To update the dependency analysis baseline file, please run: + ./gradlew $projectPath:updateDependencyAnalysisBaseline + $TERMINAL_RESET + ******************************************************************************** + """ + .trimIndent() + ) + } + } + + private fun getIncrementalAdvice(missingDependencyAdvice: List): String { + // Skip the reporting of modify dependencies for now, so that advice is easier to follow. + val unused = mutableSetOf() + val transitive = mutableSetOf() + val advice = StringBuilder() + + missingDependencyAdvice.forEach { + // Don't fail CI if test source set has misconfigured dependencies + if (it.fromConfiguration?.contains("test", ignoreCase = true) == true) { + return@forEach + } + if (it.toConfiguration?.contains("test", ignoreCase = true) == true) { + return@forEach + } + + val isCompileOnly = + it.toConfiguration?.endsWith("compileOnly", ignoreCase = true) == true + val isTransitiveDependencyAdvice = + it.fromConfiguration == null && it.toConfiguration != null && !isCompileOnly + val isUnusedDependencyAdvice = + it.fromConfiguration != null && it.toConfiguration == null + + val identifier = + if (it.coordinates.type == "project") { + "project(${it.coordinates.identifier})" + } else { + "'${it.coordinates.identifier}:${it.coordinates.resolvedVersion}'" + } + if (isTransitiveDependencyAdvice) { + transitive.add("${it.toConfiguration}($identifier)") + } + if (isUnusedDependencyAdvice) { + unused.add("${it.fromConfiguration}($identifier)") + } + } + if (unused.isNotEmpty()) { + advice.appendLine("Unused dependencies which should be removed:") + advice.appendLine(unused.sorted().joinToString(separator = "\n")) + } + if (transitive.isNotEmpty()) { + advice.appendLine("These transitive dependencies can be declared directly:") + advice.appendLine(transitive.sorted().joinToString(separator = "\n")) + } + return advice.toString() + } +} + +/** Task to update dependency analysis baselines for the project. */ +@CacheableTask +abstract class UpdateDependencyAnalysisBaseLineTask : AbstractPostProcessingTask() { + init { + group = "Verification" + description = "Task for updating dependency analysis baselines" + } + + @get:OutputFile abstract val outputFile: RegularFileProperty + @get:Internal val isKMP: Boolean = project.multiplatformExtension != null + @get:Internal + val isPublishedLibrary: Boolean = + project.extensions.getByType(AndroidXExtension::class.java).type == + SoftwareType.PUBLISHED_LIBRARY + + @TaskAction + fun updateBaseLineForDependencyAnalysisAdvice() { + val projectAdvice = + this@UpdateDependencyAnalysisBaseLineTask.projectAdvice().toAndroidxProjectAdvice() + val outputFile = outputFile.get() + val gson = GsonBuilder().setPrettyPrinting().create() + outputFile.asFile.writeText(gson.toJson(projectAdvice)) + } +} + +/** + * Configure the dependency analysis gradle plugin and register new post-processing tasks: + * 1. Updating the baselines for advice provided by the plugin. + * 2. Getting any incremental advice not captured in the baselines. + */ +internal fun Project.configureDependencyAnalysisPlugin() { + plugins.apply("com.autonomousapps.dependency-analysis") + + val updateDependencyAnalysisBaselineTask = + tasks.register( + "updateDependencyAnalysisBaseline", + UpdateDependencyAnalysisBaseLineTask::class.java, + ) { task -> + task.outputFile.set(layout.projectDirectory.file("dependencyAnalysis-baseline.json")) + task.cacheEvenIfNoOutputs() + // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved + task.onlyIf { !(task.isKMP) && task.isPublishedLibrary } + } + + val reportDependencyAnalysisAdviceTask = + tasks.register( + "reportDependencyAnalysisAdvice", + ReportDependencyAnalysisAdviceTask::class.java, + ) { task -> + task.baseLineFile.set(layout.projectDirectory.file("dependencyAnalysis-baseline.json")) + task.cacheEvenIfNoOutputs() + // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved + task.onlyIf { !(task.isKMP) && task.isPublishedLibrary } + } + + val dependencyAnalysisSubExtension = + extensions.getByType(com.autonomousapps.DependencyAnalysisSubExtension::class.java) + dependencyAnalysisSubExtension.registerPostProcessingTask(reportDependencyAnalysisAdviceTask) + dependencyAnalysisSubExtension.registerPostProcessingTask(updateDependencyAnalysisBaselineTask) + + // Ignore advice for runTimeOnly, compileOnly or incorrect dependency configs + // since it affects downstream consumers + dependencyAnalysisSubExtension.issues { it.onIncorrectConfiguration { it.severity("ignore") } } + dependencyAnalysisSubExtension.issues { it.onRuntimeOnly { it.severity("ignore") } } + dependencyAnalysisSubExtension.issues { it.onCompileOnly { it.severity("ignore") } } + + // DAGP currently doesn't support KMP, enable KMP projects when b/394970486 is resolved + // Enable CI check for published libraries + if ( + multiplatformExtension == null && + androidXExtension.type.get() == SoftwareType.PUBLISHED_LIBRARY + ) { + addToBuildOnServer(reportDependencyAnalysisAdviceTask) + } +} + +/** + * Helper data classes to store the advice provided Dependency Analysis Gradle plugin in baselines. + */ +internal data class AndroidxProjectAdvice( + val projectPath: String, + val dependencyAdvice: List, +) + +internal data class DependencyAdvice( + val coordinates: Coordinates, + val fromConfiguration: String?, + val toConfiguration: String?, +) + +internal data class Coordinates( + val type: String, + val identifier: String, + val resolvedVersion: String?, +) + +/** Convert advice reported by DAGP into format suitable for storing in baselines. */ +internal fun ProjectAdvice.toAndroidxProjectAdvice(): AndroidxProjectAdvice { + return AndroidxProjectAdvice( + projectPath = projectPath, + dependencyAdvice = + dependencyAdvice.map { + val type = + if (it.coordinates is ProjectCoordinates) { + "project" + } else { + "module" + } + val resolvedVersion = + if (it.coordinates is ModuleCoordinates) { + (it.coordinates as ModuleCoordinates).resolvedVersion + } else { + null + } + DependencyAdvice( + coordinates = + Coordinates( + identifier = it.coordinates.identifier, + resolvedVersion = resolvedVersion, + type = type, + ), + fromConfiguration = it.fromConfiguration, + toConfiguration = it.toConfiguration, + ) + }, + ) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt new file mode 100644 index 0000000000000..562129865adf0 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient +import com.google.cloud.secretmanager.v1.SecretVersionName +import java.io.File +import org.gradle.api.Project +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters + +/** + * If the user hasn't set up develocity on this machine then fetch a shared key to enable it for + * them. + */ +internal fun Project.fetchDevelocityKeysIfNeeded() { + // Playground users don't need Develocity set up + if (ProjectLayoutType.isPlayground(this)) return + + // We are in CI, so we should not fetch these keys + if (System.getenv("IS_ANDROIDX_CI") != null) return + + // User does not have remote cache enabled, so we will not have access to GCP + if (System.getenv("USE_ANDROIDX_REMOTE_BUILD_CACHE") !in setOf("gcp", "true")) return + + val keys = File("${System.getenv("GRADLE_USER_HOME")}/develocity/keys.properties") + + // User already has the keys + if (keys.exists()) return + + keys.parentFile.mkdirs() + + val keysProvider = providers.of(DevelocityKeysValueSource::class.java) {} + keys.writeText(keysProvider.get()) +} + +/** + * Using a ValueSource to fetch Develocity keys because the SecretManagerServiceClient on Macs use + * external processes (such as codesign and install_name_tool) and that is not allowed when + * configuration cache is enabled without wrapping those calls in a ValueSource. + */ +internal abstract class DevelocityKeysValueSource : + ValueSource { + override fun obtain(): String? { + var value: String? = null + try { + SecretManagerServiceClient.create().use { manager -> + val secretVersionName = + SecretVersionName.of("androidx-ge", "develocity-token", "latest") + val response = manager.accessSecretVersion(secretVersionName) + value = response.payload.data.toStringUtf8() + } + } catch (e: Exception) { + println("Failed to fetch develocity keys") + e.printStackTrace() + } + return value + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt new file mode 100644 index 0000000000000..19c4ab840900f --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt @@ -0,0 +1,323 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import com.android.build.api.variant.AndroidComponentsExtension +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.logging.Logging +import org.gradle.api.plugins.JavaPlugin.COMPILE_JAVA_TASK_NAME +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.kotlin.dsl.exclude +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.getByName +import org.gradle.process.CommandLineArgumentProvider +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation + +const val ERROR_PRONE_TASK = "runErrorProne" + +private const val ERROR_PRONE_CONFIGURATION = "errorprone" +private val log = Logging.getLogger("ErrorProneConfiguration") + +fun Project.configureErrorProneForJava() { + val errorProneConfiguration = createErrorProneConfiguration() + project.extensions.getByName("sourceSets").configureEach { + project.configurations[it.annotationProcessorConfigurationName].extendsFrom( + errorProneConfiguration + ) + } + val kmpExtension = project.multiplatformExtension + log.info("Configuring error-prone for ${project.path}") + if (kmpExtension != null) { // KMP project + val compileJavaTaskProvider = + kmpExtension + .jvm() + .compilations + .getByName(KotlinCompilation.MAIN_COMPILATION_NAME) + .compileJavaTaskProvider + makeErrorProneTask(compileJavaTaskProvider) + } else { // non-KMP project + makeErrorProneTask(tasks.withType(JavaCompile::class.java).named(COMPILE_JAVA_TASK_NAME)) + } +} + +fun Project.configureErrorProneForAndroid() { + val androidComponents = extensions.findByType(AndroidComponentsExtension::class.java) + androidComponents?.onVariants { variant -> + if (variant.buildType == "release") { + @Suppress("UnstableApiUsage", "USELESS_ELVIS") + // b/397707182 this is still @Incubating in AGP + // b/328749039 This is being made nullable in AGP + val javaCompilation = variant.javaCompilation ?: return@onVariants + val errorProneConfiguration = createErrorProneConfiguration() + configurations + .getByName(variant.annotationProcessorConfiguration.name) + .extendsFrom(errorProneConfiguration) + + log.info("Configuring error-prone for ${variant.name}'s java compile") + afterEvaluate { + makeErrorProneTask( + compileTaskProvider = + tasks + .withType(JavaCompile::class.java) + .named("compile${variant.name.camelCase()}JavaWithJavac"), + taskSuffix = variant.name.camelCase(), + ) { javaCompile -> + @Suppress("UnstableApiUsage") // JavaCompilation b/397707182 + val annotationArgs = javaCompilation.annotationProcessor.arguments + javaCompile.options.compilerArgumentProviders.add( + CommandLineArgumentProviderAdapter(annotationArgs) + ) + } + } + } + } +} + +class CommandLineArgumentProviderAdapter(@get:Input val arguments: Provider>) : + CommandLineArgumentProvider { + override fun asArguments(): MutableIterable { + return mutableListOf().also { + for ((key, value) in arguments.get()) { + it.add("-A$key=$value") + } + } + } +} + +private fun Project.createErrorProneConfiguration(): Configuration = + configurations.findByName(ERROR_PRONE_CONFIGURATION) + ?: configurations.create(ERROR_PRONE_CONFIGURATION).apply { + isCanBeConsumed = false + isCanBeResolved = true + exclude(group = "com.google.errorprone", module = "javac") + project.dependencies.add(ERROR_PRONE_CONFIGURATION, getLibraryByName("errorProne")) + } + +// Given an existing JavaCompile task, reconfigures the task to use the ErrorProne compiler plugin +private fun JavaCompile.configureWithErrorProne() { + options.isFork = true + options.forkOptions.jvmArgs!!.addAll( + listOf( + "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", + ) + ) + val compilerArgs = this.options.compilerArgs + compilerArgs += + listOf( + "--should-stop=ifError=FLOW", + // Tell error-prone that we are running it on android compatible libraries + "-XDandroidCompatible=true", + "-XDcompilePolicy=simple", // Workaround for b/36098770 + listOf( + "-Xplugin:ErrorProne", + + // : Disables warnings in classes annotated with @Generated + "-XepDisableWarningsInGeneratedCode", + + // Ignore intermediate build output, generated files, and external sources. Also + // sources + // imported from Android Studio and IntelliJ which are used in the lint-checks + // project. + "-XepExcludedPaths:.*/(build/generated|build/errorProne|external|" + + "compileTransaction/compile-output|" + + "lint-checks/src/main/java/androidx/com)/.*", + + // Consider re-enabling the following checks. Disabled as part of + // error-prone upgrade + "-Xep:InlineMeSuggester:OFF", + "-Xep:NarrowCalculation:OFF", + "-Xep:LongDoubleConversion:OFF", + "-Xep:UnicodeEscape:OFF", + "-Xep:JavaUtilDate:OFF", + "-Xep:UnrecognisedJavadocTag:OFF", + "-Xep:ObjectEqualsForPrimitives:OFF", + "-Xep:DoNotCallSuggester:OFF", + "-Xep:EqualsNull:OFF", + "-Xep:MalformedInlineTag:OFF", + "-Xep:MissingSuperCall:OFF", + "-Xep:ToStringReturnsNull:OFF", + "-Xep:ReturnValueIgnored:OFF", + "-Xep:MissingImplementsComparable:OFF", + "-Xep:EmptyTopLevelDeclaration:OFF", + "-Xep:InvalidThrowsLink:OFF", + "-Xep:StaticAssignmentOfThrowable:OFF", + "-Xep:DoNotClaimAnnotations:OFF", + "-Xep:AlreadyChecked:OFF", + "-Xep:StringSplitter:OFF", + "-Xep:NonApiType:OFF", + "-Xep:StringCaseLocaleUsage:OFF", + "-Xep:LabelledBreakTarget:OFF", + "-Xep:Finalize:OFF", + "-Xep:AddressSelection:OFF", + "-Xep:StringCharset:OFF", + "-Xep:EnumOrdinal:OFF", + "-Xep:ClassInitializationDeadlock:OFF", + "-Xep:VoidUsed:OFF", + "-Xep:EffectivelyPrivate:OFF", + "-Xep:StatementSwitchToExpressionSwitch:OFF", + "-Xep:AssignmentExpression:OFF", + "-Xep:DuplicateBranches:OFF", + "-Xep:FormatStringShouldUsePlaceholders:OFF", + "-Xep:RedundantControlFlow:OFF", + "-Xep:CollectionUndefinedEquality:OFF", + "-Xep:JavaDurationGetSecondsToToSeconds:OFF", + "-Xep:BooleanLiteral:OFF", + + // We allow inter library RestrictTo usage. + "-Xep:RestrictTo:OFF", + + // Disable the following checks. + "-Xep:UnescapedEntity:OFF", + "-Xep:MissingSummary:OFF", + "-Xep:StaticAssignmentInConstructor:OFF", + "-Xep:InvalidLink:OFF", + "-Xep:InvalidInlineTag:OFF", + "-Xep:EmptyBlockTag:OFF", + "-Xep:EmptyCatch:OFF", + "-Xep:JdkObsolete:OFF", + "-Xep:PublicConstructorForAbstractClass:OFF", + "-Xep:MutablePublicArray:OFF", + "-Xep:NonCanonicalType:OFF", + "-Xep:ModifyCollectionInEnhancedForLoop:OFF", + "-Xep:InheritDoc:OFF", + "-Xep:InvalidParam:OFF", + "-Xep:InlineFormatString:OFF", + "-Xep:InvalidBlockTag:OFF", + "-Xep:ProtectedMembersInFinalClass:OFF", + "-Xep:SameNameButDifferent:OFF", + "-Xep:AnnotateFormatMethod:OFF", + "-Xep:ReturnFromVoid:OFF", + "-Xep:AlmostJavadoc:OFF", + "-Xep:InjectScopeAnnotationOnInterfaceOrAbstractClass:OFF", + "-Xep:InvalidThrows:OFF", + + // Disable checks which are already enforced by lint. + "-Xep:PrivateConstructorForUtilityClass:OFF", + + // Enforce the following checks. + "-Xep:JavaTimeDefaultTimeZone:ERROR", + "-Xep:ParameterNotNullable:ERROR", + "-Xep:MissingOverride:ERROR", + "-Xep:EqualsHashCode:ERROR", + "-Xep:NarrowingCompoundAssignment:ERROR", + "-Xep:ClassNewInstance:ERROR", + "-Xep:ClassCanBeStatic:ERROR", + "-Xep:SynchronizeOnNonFinalField:ERROR", + "-Xep:OperatorPrecedence:ERROR", + "-Xep:IntLongMath:ERROR", + "-Xep:MissingFail:ERROR", + "-Xep:JavaLangClash:ERROR", + "-Xep:TypeParameterUnusedInFormals:ERROR", + // "-Xep:StringSplitter:ERROR", // disabled with upgrade to 2.14.0 + "-Xep:ReferenceEquality:ERROR", + "-Xep:AssertionFailureIgnored:ERROR", + "-Xep:UnnecessaryParentheses:ERROR", + "-Xep:EqualsGetClass:ERROR", + "-Xep:UnusedVariable:ERROR", + "-Xep:UnusedMethod:ERROR", + "-Xep:UndefinedEquals:ERROR", + "-Xep:ThreadLocalUsage:ERROR", + "-Xep:FutureReturnValueIgnored:ERROR", + "-Xep:ArgumentSelectionDefectChecker:ERROR", + "-Xep:HidingField:ERROR", + "-Xep:UnsynchronizedOverridesSynchronized:ERROR", + "-Xep:Finally:ERROR", + "-Xep:ThreadPriorityCheck:ERROR", + "-Xep:AutoValueFinalMethods:ERROR", + "-Xep:ImmutableEnumChecker:ERROR", + "-Xep:UnsafeReflectiveConstructionCast:ERROR", + "-Xep:LockNotBeforeTry:ERROR", + "-Xep:DoubleCheckedLocking:ERROR", + "-Xep:InconsistentCapitalization:ERROR", + "-Xep:ModifiedButNotUsed:ERROR", + "-Xep:AmbiguousMethodReference:ERROR", + "-Xep:EqualsIncompatibleType:ERROR", + "-Xep:ParameterName:ERROR", + "-Xep:RxReturnValueIgnored:ERROR", + "-Xep:BadImport:ERROR", + "-Xep:MissingCasesInEnumSwitch:ERROR", + "-Xep:ObjectToString:ERROR", + "-Xep:CatchAndPrintStackTrace:ERROR", + "-Xep:MixedMutabilityReturnType:ERROR", + + // Enforce checks related to nullness annotation usage + "-Xep:NullablePrimitiveArray:ERROR", + "-Xep:MultipleNullnessAnnotations:ERROR", + "-Xep:NullablePrimitive:ERROR", + "-Xep:NullableVoid:ERROR", + "-Xep:NullableWildcard:ERROR", + "-Xep:NullableTypeParameter:ERROR", + "-Xep:NullableConstructor:ERROR", + + // Nullaway + "-XepIgnoreUnknownCheckNames", // https://github.com/uber/NullAway/issues/25 + "-Xep:NullAway:ERROR", + "-XepOpt:NullAway:AnnotatedPackages=android.arch,android.support,androidx", + ) + .joinToString(" "), + ) +} + +/** + * Given a [JavaCompile] task, creates a task that runs the ErrorProne compiler with the same + * settings. + * + * @param onConfigure optional callback which lazily evaluates on task configuration. Use this to do + * any additional configuration such as overriding default settings. + */ +private fun Project.makeErrorProneTask( + compileTaskProvider: TaskProvider?, + taskSuffix: String = "", + onConfigure: (errorProneTask: JavaCompile) -> Unit = {}, +) = afterEvaluate { + val compileTaskProviderExists = provider { compileTaskProvider != null } + val errorProneTaskProvider = + tasks.register("$ERROR_PRONE_TASK$taskSuffix", JavaCompile::class.java) { + it.onlyIf { compileTaskProviderExists.get() } + val compileTask = compileTaskProvider?.get() ?: return@register + it.group = "Build" + it.description = "Compile this project's Java code with Error-prone compiler" + it.classpath = compileTask.classpath + it.source = compileTask.source + it.destinationDirectory.set(layout.buildDirectory.dir("errorProne/$taskSuffix")) + it.options.compilerArgs = compileTask.options.compilerArgs.toMutableList() + it.options.annotationProcessorPath = compileTask.options.annotationProcessorPath + it.options.bootstrapClasspath = compileTask.options.bootstrapClasspath + it.sourceCompatibility = compileTask.sourceCompatibility + it.targetCompatibility = compileTask.targetCompatibility + it.configureWithErrorProne() + it.dependsOn(compileTask.dependsOn) + + onConfigure(it) + } + addToCheckTask(errorProneTaskProvider) + addToBuildOnServer(errorProneTaskProvider) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt new file mode 100644 index 0000000000000..73aca8299419d --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.FilteredAnchorTask.Companion.GLOBAL_TASK_NAME +import androidx.build.FilteredAnchorTask.Companion.PROP_PATH_PREFIX +import androidx.build.FilteredAnchorTask.Companion.PROP_TASK_NAME +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.work.DisableCachingByDefault + +@DisableCachingByDefault(because = "This is an anchor task that does no work.") +abstract class FilteredAnchorTask : DefaultTask() { + init { + group = "Help" + description = + "Runs tasks with a name specified by -P$PROP_TASK_NAME= for projects with " + + "a path prefix specified by -P$PROP_PATH_PREFIX=" + } + + @get:Input abstract var pathPrefix: String + + @get:Input abstract var taskName: String + + @TaskAction + fun exec() { + if (dependsOn.isEmpty()) { + throw GradleException( + "Failed to find any filterable tasks with name \"$taskName\" " + + "and path prefixed with \"$pathPrefix\"" + ) + } + } + + companion object { + const val GLOBAL_TASK_NAME = "filterTasks" + const val PROP_PATH_PREFIX = "androidx.pathPrefix" + const val PROP_TASK_NAME = "androidx.taskName" + } +} + +/** + * Offers the specified [taskProviders] to the global [FilteredAnchorTask], adding them if they + * match the requested path prefix and task name. + */ +internal fun Project.addFilterableTasks(vararg taskProviders: TaskProvider<*>?) { + if ( + providers.gradleProperty(PROP_PATH_PREFIX).isPresent && + providers.gradleProperty(PROP_TASK_NAME).isPresent + ) { + val pathPrefixes = (properties[PROP_PATH_PREFIX] as String).split(",") + if (pathPrefixes.any { pathPrefix -> relativePathForFiltering().startsWith(pathPrefix) }) { + val taskName = properties[PROP_TASK_NAME] as String + taskProviders + .find { taskProvider -> taskName == taskProvider?.name } + ?.let { taskProvider -> + rootProject.tasks.named(GLOBAL_TASK_NAME).configure { task -> + task.dependsOn(taskProvider) + } + } + } + } +} + +/** + * Registers the global [FilteredAnchorTask] if the required command-line properties are set. + * + * For example, to run `checkApi` for all projects under `core/core/`: ./gradlew filterTasks + * -Pandroidx.taskName=checkApi -Pandroidx.pathPrefix=core/core/ + */ +internal fun Project.maybeRegisterFilterableTask() { + if ( + providers.gradleProperty(PROP_TASK_NAME).isPresent && + providers.gradleProperty(PROP_PATH_PREFIX).isPresent + ) { + tasks.register(GLOBAL_TASK_NAME, FilteredAnchorTask::class.java) { task -> + task.pathPrefix = properties[PROP_PATH_PREFIX] as String + task.taskName = properties[PROP_TASK_NAME] as String + } + } +} + +/** + * Returns an AndroidX-relative path for the [Project], inserting the root project directory when + * run in a Playground context such that paths are consistent with the AndroidX context. + */ +internal fun Project.relativePathForFiltering(): String = + if (ProjectLayoutType.isPlayground(project)) { + "${projectDir.relativeTo(getSupportRootFolder())}/" + } else { + "${projectDir.relativeTo(rootDir)}/" + } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt new file mode 100644 index 0000000000000..fe60ce9da74c8 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/FtlRunner.kt @@ -0,0 +1,333 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import com.android.build.api.artifact.Artifacts +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.BuiltArtifactsLoader +import com.android.build.api.variant.HasDeviceTests +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.kotlin.dsl.getByType +import org.gradle.process.ExecOperations +import org.gradle.process.ExecSpec +import org.gradle.work.DisableCachingByDefault + +@DisableCachingByDefault(because = "Expected to rerun every time") +abstract class FtlRunner : DefaultTask() { + init { + group = "Verification" + description = "Runs devices tests in Firebase Test Lab filtered by --className" + } + + @get:Inject abstract val execOperations: ExecOperations + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val testFolder: DirectoryProperty + + @get:Internal abstract val testLoader: Property + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:Optional + abstract val appFolder: DirectoryProperty + + @get:Internal abstract val appLoader: Property + + @get:Input abstract val apkPackageName: Property + + @get:Optional + @get:Input + @get:Option(option = "className", description = "Fully qualified class name of a class to run") + abstract val className: Property + + @get:Optional + @get:Input + @get:Option(option = "packageName", description = "Package name test classes to run") + abstract val packageName: Property + + @get:Optional + @get:Input + @get:Option(option = "pullScreenshots", description = "true if screenshots should be pulled") + abstract val pullScreenshots: Property + + @get:Optional + @get:Input + @get:Option(option = "testTimeout", description = "timeout to pass to FTL test runner") + abstract val testTimeout: Property + + @get:Optional + @get:Input + @get:Option( + option = "instrumentationArgs", + description = "instrumentation arguments to pass to FTL test runner", + ) + abstract val instrumentationArgs: Property + + @get:Optional + @get:Input + @get:Option( + option = "api", + description = + "repeatable argument for which apis to run ftl tests on. " + + "Only relevant to $FTL_ON_APIS_NAME. Can be 23, 26, 28, 30, 33, 34, 35.", + ) + abstract val apis: ListProperty + + @get:Optional + @get:Input + @get:Option( + option = "shardCount", + description = "Number of shards to split tests into (requires gcloud beta)", + ) + abstract val shardCount: Property + + @get:Optional + @get:Input + @get:Option( + option = "excludeAnnotation", + description = + "Repeatable argument to exclude annotations. " + + "Example: `--excludeAnnotation androidx.test.filters.FlakyTest`", + ) + abstract val excludeAnnotations: ListProperty + + @get:Input abstract val device: ListProperty + + @TaskAction + fun execThings() { + if (!System.getenv().containsKey("GOOGLE_APPLICATION_CREDENTIALS")) { + throw Exception( + "Running tests in FTL requires credentials, you have not set up " + + "GOOGLE_APPLICATION_CREDENTIALS, follow go/androidx-dev#remote-build-cache" + ) + } + val testApk = + testLoader.get().load(testFolder.get()) + ?: throw RuntimeException("Cannot load required APK for task: $name") + val testApkPath = testApk.elements.single().outputFile + val appApkPath = + if (appLoader.isPresent) { + val appApk = + appLoader.get().load(appFolder.get()) + ?: throw RuntimeException("Cannot load required APK for task: $name") + appApk.elements.single().outputFile + } else { + "gs://androidx-ftl-test-results/github-ci-action/placeholderApp/" + + "d345c82828c355acc1432535153cf1dcf456e559c26f735346bf5f38859e0512.apk" + } + try { + execOperations.printCommandAndExec { it.commandLine("gcloud", "--version") } + } catch (_: Exception) { + throw Exception( + "Missing gcloud, please follow go/androidx-dev#remote-build-cache to set it up" + ) + } + + val filterList = buildList { + if (className.isPresent) add("class ${className.get()}") + if (packageName.isPresent) add("package ${packageName.get()}") + if (excludeAnnotations.isPresent) { + addAll(excludeAnnotations.get().map { "notAnnotation $it" }) + } + } + val hasFilters = filterList.isNotEmpty() + val filters = filterList.joinToString(separator = ",") + + val shouldPull = pullScreenshots.isPresent && pullScreenshots.get() == "true" + + val needsBeta = shardCount.isPresent + execOperations.printCommandAndExec { + it.commandLine( + listOfNotNull( + "gcloud", + if (needsBeta) "beta" else null, + "--project", + "androidx-dev-prod", + "firebase", + "test", + "android", + "run", + "--type", + "instrumentation", + "--no-performance-metrics", + "--no-auto-google-login", + "--app", + appApkPath, + "--test", + testApkPath, + "--results-bucket=androidx-dev-prod-test-results", + if (hasFilters) "--test-targets" else null, + if (hasFilters) filters else null, + if (shouldPull) "--directories-to-pull" else null, + if (shouldPull) { + "/sdcard/Android/data/${apkPackageName.get()}/cache/androidx_screenshots" + } else null, + if (testTimeout.isPresent) "--timeout" else null, + if (testTimeout.isPresent) testTimeout.get() else null, + if (shardCount.isPresent) "--num-uniform-shards" else null, + if (shardCount.isPresent) shardCount.get() else null, + if (instrumentationArgs.isPresent) "--environment-variables" else null, + if (instrumentationArgs.isPresent) instrumentationArgs.get() else null, + ) + getDeviceArguments() + ) + } + } + + private fun getDeviceArguments(): List { + val devices = device.get().ifEmpty { readApis() } + return devices.flatMap { listOf("--device", "model=$it,locale=en_US,orientation=portrait") } + } + + private fun readApis(): Collection { + val apis = apis.get() + if (apis.isEmpty()) { + throw RuntimeException("--api must be specified when using $FTL_ON_APIS_NAME.") + } + + val apisWithoutModels = apis.filter { it !in API_TO_MODEL_MAP } + if (apisWithoutModels.isNotEmpty()) { + throw RuntimeException("Unknown apis specified: ${apisWithoutModels.joinToString()}") + } + + return apis.map { API_TO_MODEL_MAP[it]!! } + } +} + +private const val NEXUS_6P = "Nexus6P,version=27" +private const val A10 = "a10,version=29" +private const val PETTYL = "pettyl,version=27" +private const val HWCOR = "HWCOR,version=27" +private const val Q2Q = "q2q,version=31" + +private const val PHYSICAL_PIXEL9 = "tokay,version=34" +private const val MEDIUM_PHONE_36 = "MediumPhone.arm,version=36" +private const val MEDIUM_PHONE_35 = "MediumPhone.arm,version=35" +private const val MEDIUM_PHONE_34 = "MediumPhone.arm,version=34" +private const val MEDIUM_PHONE_33 = "MediumPhone.arm,version=33" +private const val MEDIUM_PHONE_30 = "MediumPhone.arm,version=30" +private const val MEDIUM_PHONE_28 = "MediumPhone.arm,version=28" +private const val MEDIUM_PHONE_26 = "MediumPhone.arm,version=26" +private const val NEXUS5_23 = "Nexus5.gce_x86,version=23" +private const val PIXEL2_33 = "Pixel2.arm,version=33" +private const val PIXEL2_30 = "Pixel2.arm,version=30" +private const val PIXEL2_28 = "Pixel2.arm,version=28" +private const val PIXEL2_26 = "Pixel2.arm,version=26" + +private val API_TO_MODEL_MAP = + mapOf( + 36 to MEDIUM_PHONE_36, + 35 to MEDIUM_PHONE_35, + 34 to MEDIUM_PHONE_34, + 33 to MEDIUM_PHONE_33, + 30 to MEDIUM_PHONE_30, + 28 to MEDIUM_PHONE_28, + 26 to MEDIUM_PHONE_26, + 23 to NEXUS5_23, + ) + +private const val FTL_ON_APIS_NAME = "ftlOnApis" +private val devicesToRunOn = + listOf( + FTL_ON_APIS_NAME to listOf(), // instead read devices via repeatable --api + "ftlphysicalpixel9api34" to listOf(PHYSICAL_PIXEL9), + "ftlmediumphoneapi36" to listOf(MEDIUM_PHONE_36), + "ftlmediumphoneapi35" to listOf(MEDIUM_PHONE_35), + "ftlmediumphoneapi34" to listOf(MEDIUM_PHONE_34), + "ftlmediumphoneapi33" to listOf(MEDIUM_PHONE_33), + "ftlmediumphoneapi30" to listOf(MEDIUM_PHONE_30), + "ftlmediumphoneapi28" to listOf(MEDIUM_PHONE_28), + "ftlmediumphoneapi26" to listOf(MEDIUM_PHONE_26), + "ftlnexus5api23" to listOf(NEXUS5_23), + "ftlCoreTelecomDeviceSet" to listOf(NEXUS_6P, A10, PETTYL, HWCOR, Q2Q), + "ftlpixel2api33" to listOf(PIXEL2_33), + "ftlpixel2api30" to listOf(PIXEL2_30), + "ftlpixel2api28" to listOf(PIXEL2_28), + "ftlpixel2api26" to listOf(PIXEL2_26), + ) + +internal fun Project.registerRunner( + name: String, + artifacts: Artifacts, + namespace: Provider, +) { + devicesToRunOn.forEach { (taskPrefix, model) -> + tasks.register("$taskPrefix$name", FtlRunner::class.java) { task -> + task.device.set(model) + task.apkPackageName.set(namespace) + task.testFolder.set(artifacts.get(SingleArtifact.APK)) + task.testLoader.set(artifacts.getBuiltArtifactsLoader()) + } + } +} + +fun Project.configureFtlRunner(androidComponentsExtension: AndroidComponentsExtension<*, *, *>) { + androidComponentsExtension.apply { + onVariants { variant -> + when { + variant is HasDeviceTests -> { + variant.deviceTests.forEach { (_, deviceTest) -> + registerRunner(deviceTest.name, deviceTest.artifacts, deviceTest.namespace) + } + } + project.plugins.hasPlugin("com.android.test") -> { + registerRunner(variant.name, variant.artifacts, variant.namespace) + } + } + } + } +} + +fun Project.addAppApkToFtlRunner() { + extensions.getByType().apply { + onVariants(selector().withBuildType("debug")) { appVariant -> + devicesToRunOn.forEach { (taskPrefix, _) -> + tasks.named("$taskPrefix${appVariant.name}AndroidTest") { configTask -> + configTask as FtlRunner + configTask.appFolder.set(appVariant.artifacts.get(SingleArtifact.APK)) + configTask.appLoader.set(appVariant.artifacts.getBuiltArtifactsLoader()) + } + } + } + } +} + +private fun ExecOperations.printCommandAndExec(action: (ExecSpec) -> Unit) { + exec { spec -> + action(spec) + + // Just approximating the command for user verification. + val commandLine = spec.commandLine.map { if (" " in it) "\"$it\"" else it } + println("Executing command: `${commandLine.joinToString(" ")}`") + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/GradleTransformWorkaround.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/GradleTransformWorkaround.kt new file mode 100644 index 0000000000000..73ea330cd483f --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/GradleTransformWorkaround.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.gradle.isRoot +import org.gradle.api.Project + +/** + * Creates a dependency substitution rule to workaround + * [a Gradle bug](https://github.com/gradle/gradle/issues/20778). + * + * The root cause of the bug is a mix of external and project coordinates existing simultaneously in + * the dependency graph. A Gradle optimization attempts to simplify/minimize this graph to allow + * artifact transforms to being executing as soon as possible, but the optimization was too + * aggressive in the Androidx case. + * + * This workaround creates a no-op/unmatching rule which invalidates the above optimization and + * prevents transformations from executing too eagerly. + * + * This is necessary for Gradle 7.5-rc-1, but should be fixed in Gradle 7.5.1 or 7.6, at which point + * this class can be removed. + */ +object GradleTransformWorkaround { + /** + * This function applies the [GradleTransformWorkaround] to the given root project, if necessary + * (if it includes lifecycle-common). + * + * @param rootProject The root project whose sub-projects will be updated with the workaround. + */ + fun maybeApply(rootProject: Project) { + check(rootProject.isRoot) { + """ + GradleTransformWorkaround must be invoked with the root project + because it needs to be applied to all sub-projects. + """ + .trimIndent() + } + rootProject.subprojects { subProject -> + if (subProject.path == ":lifecycle:lifecycle-common") { + rootProject.subprojects { it.applyArtifactTransformWorkaround() } + } + } + } + + private fun Project.applyArtifactTransformWorkaround() { + this.configurations.configureEach { c -> + c.resolutionStrategy.dependencySubstitution { selector -> + selector + .substitute(selector.module("unmatched:unmatched")) + .using(selector.project(":lifecycle:lifecycle-common")) + .because( + "workaround gradle/gradle#20778 with intentionally unmatching " + + "substitution rule" + ) + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/InspectionRelease.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/InspectionRelease.kt new file mode 100644 index 0000000000000..0fcbeaf70d5c6 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/InspectionRelease.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.inspection.gradle.InspectionExtension +import androidx.inspection.gradle.InspectionPlugin +import androidx.inspection.gradle.createConsumeInspectionConfiguration +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration + +/** Copies artifacts prepared by InspectionPlugin into $destDir/inspection */ +fun Project.publishInspectionArtifacts() { + project.afterEvaluate { + if (project.plugins.hasPlugin(InspectionPlugin::class.java)) { + publishInspectionConfiguration( + "copyInspectionArtifacts", + createConsumeInspectionConfiguration(), + "inspection", + ) + } + } +} + +internal fun Project.publishInspectionConfiguration( + name: String, + configuration: Configuration, + dirName: String, +) { + project.dependencies.add(configuration.name, project) + val sync = + tasks.register(name, SingleFileCopy::class.java) { + it.dependsOn(configuration) + it.sourceFile.set(project.files(configuration).singleFile) + val extension = project.extensions.getByType(InspectionExtension::class.java) + val fileName = extension.name ?: "${project.name}.jar" + it.destinationFile.set(getDistributionDirectory().file("$dirName/$fileName")) + } + addToBuildOnServer(sync) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/JavaFormat.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/JavaFormat.kt new file mode 100644 index 0000000000000..c8107a2aad686 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/JavaFormat.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileTree +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.process.ExecOperations + +fun Project.configureJavaFormat() { + tasks.register("javaFormat", JavaFormatTask::class.java) { task -> + task.javaFormatClasspath.from(getLibraryClasspath("googlejavaformat")) + } +} + +@CacheableTask +abstract class JavaFormatTask : DefaultTask() { + init { + description = "Fix Java code style deviations." + group = "formatting" + } + + @get:Input + @set:Option(option = "fix-imports-only", description = "Only correct imports") + var importsOnly: Boolean = false + + @get:Inject abstract val execOperations: ExecOperations + + @get:Classpath abstract val javaFormatClasspath: ConfigurableFileCollection + + @get:Inject abstract val objects: ObjectFactory + + @[InputFiles PathSensitive(PathSensitivity.RELATIVE) SkipWhenEmpty IgnoreEmptyDirectories] + open fun getInputFiles(): FileTree { + return objects.fileTree().setDir(INPUT_DIR).apply { + include(INCLUDED_FILES) + exclude(excludedDirectoryGlobs) + } + } + + // Format task rewrites inputs, so the outputs are the same as inputs. + @OutputFiles fun getRewrittenFiles(): FileTree = getInputFiles() + + private fun getArgsList(): List { + val arguments = mutableListOf("--aosp", "--replace") + if (importsOnly) arguments.add("--fix-imports-only") + arguments.addAll(getInputFiles().files.map { it.absolutePath }) + return arguments + } + + @TaskAction + fun runFormat() { + execOperations.javaexec { javaExecSpec -> + javaExecSpec.mainClass.set(MAIN_CLASS) + javaExecSpec.classpath = javaFormatClasspath + javaExecSpec.args = getArgsList() + javaExecSpec.jvmArgs( + "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + ) + } + } + + companion object { + private val excludedDirectories = listOf("test-data", "external") + + private val excludedDirectoryGlobs = excludedDirectories.map { "**/$it/**/*.java" } + private const val MAIN_CLASS = "com.google.googlejavaformat.java.Main" + private const val INPUT_DIR = "src" + private const val INCLUDED_FILES = "**/*.java" + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt new file mode 100644 index 0000000000000..6abf9d5567a76 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/KonanPrebuiltsSetup.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.gradle.extraPropertyOrNull +import java.io.File +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.tasks.CInteropProcess +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile +import org.jetbrains.kotlin.konan.target.Distribution + +/** Helper class to override Konan prebuilts directories to use local konan prebuilts. */ +object KonanPrebuiltsSetup { + /** + * Flag to notify we've updated the konan properties so that we can avoid re-doing it if + * [configureKonanDirectory] call comes from multiple code paths. + */ + private const val DID_SETUP_KONAN_PROPERTIES_FLAG = "androidx.didSetupKonanProperties" + + /** + * Flag that causes konan to run in a separate process whose working directory is the compiling + * project (i.e. frameworks/support/room3/room3-runtime) and not the root project + * (frameworks/support). + */ + private const val DISABLE_COMPILER_DAEMON_FLAG = "kotlin.native.disableCompilerDaemon" + + /** + * Creates a Konan distribution with the given [prebuiltsDirectory] and [konanHome]. + * + * @param prebuiltsDirectory The directory where AndroidX prebuilts are present. Can be `null` + * for playground builds which means we'll fetch Kotlin Native prebuilts from the internet + * using the Kotlin Gradle Plugin. + */ + fun createKonanDistribution(prebuiltsDirectory: File?, konanHome: File) = + Distribution( + konanHome = konanHome.canonicalPath, + onlyDefaultProfiles = false, + propertyOverrides = + prebuiltsDirectory?.let { mapOf("dependenciesUrl" to "file://${it.canonicalPath}") }, + ) + + /** Returns `true` if the project's konan prebuilts is already configured. */ + fun isConfigured(project: Project): Boolean { + return project.extensions.extraProperties.has(DID_SETUP_KONAN_PROPERTIES_FLAG) + } + + /** Sets the konan distribution url to the prebuilts directory. */ + fun configureKonanDirectory(project: Project) { + check(!isConfigured(project)) { + "Konan prebuilts directories for project ${project.path} are already configured" + } + if (ProjectLayoutType.isPlayground(project)) { + // playground does not use prebuilts + } else { + // set konan prebuilts download URLs to AndroidX prebuilts + project.overrideKotlinNativeDistributionUrlToLocalDirectory() + project.overrideKotlinNativeDependenciesUrlToLocalDirectory() + } + project.extensions.extraProperties.set(DID_SETUP_KONAN_PROPERTIES_FLAG, true) + } + + private fun Project.overrideKotlinNativeDependenciesUrlToLocalDirectory() { + val compilerDaemonDisabled = + extraPropertyOrNull(DISABLE_COMPILER_DAEMON_FLAG)?.toString()?.toBoolean() == true + val konanPrebuiltsFolder = getKonanPrebuiltsFolder() + val rootBaseDir = if (compilerDaemonDisabled) projectDir else rootProject.projectDir + // use relative path so it doesn't affect gradle remote cache. + val relativeRootPath = konanPrebuiltsFolder.relativeTo(rootBaseDir).path + val relativeProjectPath = konanPrebuiltsFolder.relativeTo(projectDir).path + tasks.withType(KotlinNativeCompile::class.java).configureEach { + it.compilerOptions.freeCompilerArgs.add( + "-Xoverride-konan-properties=dependenciesUrl=file:$relativeRootPath" + ) + } + tasks.withType(CInteropProcess::class.java).configureEach { + it.settings.extraOpts += + listOf("-Xoverride-konan-properties", "dependenciesUrl=file:$relativeProjectPath") + } + } + + private fun Project.overrideKotlinNativeDistributionUrlToLocalDirectory() { + val url = + "file:${getKonanPrebuiltsFolder().resolve("nativeCompilerPrebuilts").absolutePath}" + extensions.extraProperties["kotlin.native.distribution.baseDownloadUrl"] = url + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/Ktfmt.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/Ktfmt.kt new file mode 100644 index 0000000000000..146f6a78403b4 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/Ktfmt.kt @@ -0,0 +1,292 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.logging.TERMINAL_RED +import androidx.build.logging.TERMINAL_RESET +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.file.Paths +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.attributes.java.TargetJvmEnvironment +import org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.file.FileTree +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.kotlin.dsl.named +import org.gradle.process.ExecOperations + +fun Project.configureKtfmt() { + val ktfmtClasspath = getKtfmtConfiguration() + tasks.register("ktFormat", KtfmtFormatTask::class.java) { task -> + task.ktfmtClasspath.from(ktfmtClasspath) + } + + val ktCheckTask = + tasks.register("ktCheck", KtfmtCheckTask::class.java) { task -> + task.ktfmtClasspath.from(ktfmtClasspath) + task.cacheEvenIfNoOutputs() + } + + // afterEvaluate because Gradle's default "check" task doesn't exist yet + afterEvaluate { + addToCheckTask(ktCheckTask) + addToBuildOnServer(ktCheckTask) + } +} + +private val ExcludedDirectories = listOf("test-data", "external") + +private val ExcludedDirectoryGlobs = ExcludedDirectories.map { "**/$it/**/*.kt" } +private const val MainClass = "com.facebook.ktfmt.cli.Main" +private const val InputDir = "src" +private const val IncludedFiles = "**/*.kt" + +private fun Project.getKtfmtConfiguration(): FileCollection { + val conf = configurations.detachedConfiguration(dependencies.create(getLibraryByName("ktfmt"))) + conf.attributes { + it.attribute( + TARGET_JVM_ENVIRONMENT_ATTRIBUTE, + project.objects.named(TargetJvmEnvironment.STANDARD_JVM), + ) + } + return conf.incoming.files +} + +@CacheableTask +abstract class BaseKtfmtTask : DefaultTask() { + @get:Inject abstract val execOperations: ExecOperations + + @get:Classpath abstract val ktfmtClasspath: ConfigurableFileCollection + + @get:Inject abstract val objects: ObjectFactory + + @get:Internal val projectPath: String = project.path + + @[InputFiles PathSensitive(PathSensitivity.RELATIVE) SkipWhenEmpty] + open fun getInputFiles(): FileTree { + val projectDirectory = overrideDirectory + val subdirectories = overrideSubdirectories + if (projectDirectory == null || subdirectories.isNullOrEmpty()) { + // If we have a valid override, use that as the default fileTree + return objects.fileTree().setDir(InputDir).apply { + include(IncludedFiles) + exclude(ExcludedDirectoryGlobs) + } + } + return objects.fileTree().setDir(projectDirectory).apply { + subdirectories.forEach { include("$it/src/**/*.kt") } + } + } + + /** Allows overriding to use a custom directory instead of default [Project.getProjectDir]. */ + @get:Internal var overrideDirectory: File? = null + + /** + * Used together with [overrideDirectory] to specify which specific subdirectories should be + * analyzed. + */ + @get:Internal var overrideSubdirectories: List? = null + + protected fun runKtfmt(format: Boolean) { + if (getInputFiles().files.isEmpty()) return + val outputStream = ByteArrayOutputStream() + val errorStream = ByteArrayOutputStream() + execOperations.javaexec { javaExecSpec -> + javaExecSpec.standardOutput = outputStream + javaExecSpec.errorOutput = errorStream + javaExecSpec.mainClass.set(MainClass) + javaExecSpec.classpath = ktfmtClasspath + javaExecSpec.args = getArgsList(format = format) + javaExecSpec.jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + overrideDirectory?.let { javaExecSpec.workingDir = it } + } + + // https://github.com/facebook/ktfmt/blob/9830466327b72879808b0d6266d2cc69ef0197b2/core/src/main/java/com/facebook/ktfmt/cli/Main.kt#L168 + // Info messages are printed to error, filter these out to avoid stderr clutter. + val error = + errorStream + .toString() + .lines() + .filterNot { it.startsWith("Done formatting ") } + .joinToString(separator = "\n") + + if (error.isNotBlank()) { + System.err.println(error) + } + + val output = outputStream.toString() + if (output.isNotEmpty()) { + error(processOutput(output)) + } + } + + open fun processOutput(output: String): String = + """ + Failed check for the following files: + $output + """ + .trimIndent() + + private fun getArgsList(format: Boolean): List { + val arguments = mutableListOf("--kotlinlang-style") + if (!format) arguments.add("--dry-run") + arguments.addAll(getInputFiles().files.map { it.absolutePath }) + return arguments + } +} + +@CacheableTask +abstract class KtfmtFormatTask : BaseKtfmtTask() { + init { + description = "Fix Kotlin code style deviations." + group = "formatting" + } + + // Format task rewrites inputs, so the outputs are the same as inputs. + @OutputFiles fun getRewrittenFiles(): FileTree = getInputFiles() + + @TaskAction + fun runFormat() { + runKtfmt(format = true) + } +} + +@CacheableTask +abstract class KtfmtCheckTask : BaseKtfmtTask() { + init { + description = "Check Kotlin code style." + group = "Verification" + } + + @TaskAction + fun runCheck() { + runKtfmt(format = false) + } + + override fun processOutput(output: String): String = + """ + Failed check for the following files: + $output + + ******************************************************************************** + ${TERMINAL_RED}You can automatically fix these issues with: + ./gradlew $projectPath:ktFormat$TERMINAL_RESET + ******************************************************************************** + """ + .trimIndent() +} + +@CacheableTask +abstract class KtfmtCheckFileTask : BaseKtfmtTask() { + init { + description = "Check Kotlin code style." + group = "Verification" + } + + @get:Internal val projectDir = project.projectDir + + @get:Input + @set:Option( + option = "file", + description = + "File to check. This option can be used multiple times: --file file1.kt " + + "--file file2.kt", + ) + var files: List = emptyList() + + @get:Input + @set:Option( + option = "format", + description = + "Use --format to auto-correct style violations (if some errors cannot be " + + "fixed automatically they will be printed to stderr)", + ) + var format = false + + override fun getInputFiles(): FileTree { + if (files.isEmpty()) { + return objects.fileTree().setDir(projectDir).apply { exclude("**") } + } + val kotlinFiles = + files + .filter { file -> + val isKotlinFile = file.endsWith(".kt") || file.endsWith(".ktx") + val inExcludedDir = + Paths.get(file).any { subPath -> + ExcludedDirectories.contains(subPath.toString()) + } + + isKotlinFile && !inExcludedDir + } + .map { it.replace("./", "**/") } + + if (kotlinFiles.isEmpty()) { + return objects.fileTree().setDir(projectDir).apply { exclude("**") } + } + return objects.fileTree().setDir(projectDir).apply { include(kotlinFiles) } + } + + @TaskAction + fun runCheck() { + runKtfmt(format = format) + } + + override fun processOutput(output: String): String { + val kotlinFiles = + files.filter { file -> + val isKotlinFile = file.endsWith(".kt") || file.endsWith(".ktx") + val inExcludedDir = + Paths.get(file).any { subPath -> + ExcludedDirectories.contains(subPath.toString()) + } + + isKotlinFile && !inExcludedDir + } + return """ + Failed check for the following files: + $output + + ******************************************************************************** + ${TERMINAL_RED}You can attempt to automatically fix these issues with: + ./gradlew :ktCheckFile --format ${kotlinFiles.joinToString(separator = " "){ "--file $it" }}$TERMINAL_RESET + ******************************************************************************** + """ + .trimIndent() + } +} + +fun Project.configureKtfmtCheckFile() { + tasks.register("ktCheckFile", KtfmtCheckFileTask::class.java) { task -> + task.ktfmtClasspath.from(getKtfmtConfiguration()) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt new file mode 100644 index 0000000000000..8de86aa297551 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import org.tomlj.Toml +import org.tomlj.TomlParseResult +import org.tomlj.TomlTable + +/** Loads Library groups and versions from a specified TOML file. */ +abstract class LibraryVersionsService : BuildService { + interface Parameters : BuildServiceParameters { + var tomlFileName: String + var tomlFileContents: Provider + } + + private val parsedTomlFile: TomlParseResult by lazy { + val result = Toml.parse(parameters.tomlFileContents.get()) + if (result.hasErrors()) { + val issues = + result.errors().joinToString(separator = "\n") { + "${parameters.tomlFileName}:${it.position()}: ${it.message}" + } + throw Exception("${parameters.tomlFileName} file has issues.\n$issues") + } + result + } + + private fun getTable(key: String): TomlTable { + return parsedTomlFile.getTable(key) + ?: throw GradleException("Library versions toml file is missing [$key] table") + } + + // map from name of constant to Version + val libraryVersions: Map by lazy { + val versions = getTable("versions") + versions.keySet().associateWith { versionName -> + val versionValue = versions.getString(versionName)!! + Version.parseOrNull(versionValue) + ?: throw GradleException( + "$versionName does not match expected format - $versionValue" + ) + } + } + + // map of library groups keyed by their variable name in the toml file + val libraryGroups: Map by lazy { + val result = mutableMapOf() + for (association in libraryGroupAssociations) { + result[association.declarationName] = association.libraryGroup + } + result + } + + // map of library groups keyed by group name + val libraryGroupsByGroupId: Map by lazy { + val result = mutableMapOf() + for (association in libraryGroupAssociations) { + // Check for duplicate groups + val groupId = association.libraryGroup.group + val existingAssociation = result[groupId] + if (existingAssociation != null) { + if ( + existingAssociation.atomicGroupVersion != null && + association.libraryGroup.atomicGroupVersion != null && + existingAssociation.group !in ALLOWED_ATOMIC_GROUP_EXCEPTIONS + ) { + throw GradleException( + "Multiple atomic groups defined with the same Maven group ID: $groupId" + ) + } + if (association.overrideIncludeInProjectPaths.isEmpty()) { + throw GradleException( + "Duplicate library group $groupId defined in " + + "${association.declarationName} does not set overrideInclude. " + + "Declarations beyond the first can only have an effect if they set " + + "overrideInclude" + ) + } + } else { + result[groupId] = association.libraryGroup + } + } + result + } + + // map from project name to group override if applicable + val overrideLibraryGroupsByProjectPath: Map by lazy { + val result = mutableMapOf() + for (association in libraryGroupAssociations) { + for (overridePath in association.overrideIncludeInProjectPaths) { + result[overridePath] = association.libraryGroup + } + } + result + } + + private val libraryGroupAssociations: List by lazy { + val groups = getTable("groups") + + fun readGroupVersion(groupDefinition: TomlTable, groupName: String, key: String): Version? { + val versionRef = groupDefinition.getString(key) ?: return null + if (!versionRef.startsWith(VersionReferencePrefix)) { + throw GradleException( + "Group entry $key is expected to start with $VersionReferencePrefix" + ) + } + // name without `versions.` + val atomicGroupVersionName = versionRef.removePrefix(VersionReferencePrefix) + return libraryVersions[atomicGroupVersionName] + ?: error( + "Group entry $groupName specifies $atomicGroupVersionName, but such version " + + "doesn't exist" + ) + } + groups.keySet().sorted().map { name -> + // get group name + val groupDefinition = groups.getTable(name)!! + val groupName = groupDefinition.getString("group")!! + + // get group version, if any + val atomicGroupVersion = + readGroupVersion( + groupDefinition = groupDefinition, + groupName = groupName, + key = AtomicGroupVersion, + ) + val overrideApplyToProjects = + (groupDefinition.getArray("overrideInclude")?.toList() ?: listOf()).map { + it as String + } + + val group = LibraryGroup(groupName, atomicGroupVersion) + LibraryGroupAssociation(name, group, overrideApplyToProjects) + } + } + + companion object { + internal fun registerOrGet(project: Project): Provider { + val tomlFileName = "libraryversions.toml" + val toml = project.lazyReadFile(tomlFileName) + + return project.gradle.sharedServices.registerIfAbsent( + "libraryVersionsService", + LibraryVersionsService::class.java, + ) { spec -> + spec.parameters.tomlFileName = tomlFileName + spec.parameters.tomlFileContents = toml + } + } + } +} + +// a LibraryGroupSpec knows how to associate a LibraryGroup with the appropriate projects +private data class LibraryGroupAssociation( + // the name of the variable to which it is assigned in the toml file + val declarationName: String, + // the group + val libraryGroup: LibraryGroup, + // the paths of any additional projects that this group should be assigned to + val overrideIncludeInProjectPaths: List, +) + +private const val VersionReferencePrefix = "versions." +private const val AtomicGroupVersion = "atomicGroupVersion" + +// Maven groups that should be skipped for atomic duplication checks. Do not add further entries. +// TODO(b/401002936, b/401000219, b/401003097, b/401005632): Remove groups from this list +private val ALLOWED_ATOMIC_GROUP_EXCEPTIONS = + listOf( + "androidx.camera", + "androidx.compose.material3", + "androidx.lifecycle", + "androidx.tracing", + ) diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt new file mode 100644 index 0000000000000..7684ebdb2cf42 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt @@ -0,0 +1,313 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.build + +import androidx.build.checkapi.shouldConfigureApiTasks +import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget +import com.android.build.api.dsl.Lint +import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension +import com.android.build.api.variant.LintLifecycleExtension +import com.android.build.gradle.AppPlugin +import com.android.build.gradle.LibraryPlugin +import com.android.build.gradle.api.KotlinMultiplatformAndroidPlugin +import java.io.File +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.plugins.JavaPlugin +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin + +/** Single entry point to Android Lint configuration. */ +fun Project.configureLint() { + project.plugins.configureEach { plugin -> + when (plugin) { + is AppPlugin -> configureAndroidProjectForLint(isLibrary = false) + is LibraryPlugin -> configureAndroidProjectForLint(isLibrary = true) + is KotlinMultiplatformAndroidPlugin -> + configureAndroidMultiplatformProjectForLint( + extensions.getByType().agpKmpExtension, + extensions.getByType(), + ) + // Only configure non-multiplatform Java projects via JavaPlugin. Multiplatform + // projects targeting Java (e.g. `jvm { withJava() }`) are configured via + // KotlinBasePlugin. + is JavaPlugin -> + if (project.multiplatformExtension == null) { + configureNonAndroidProjectForLint() + } + // Only configure non-Android multiplatform projects via KotlinBasePlugin. + // Multiplatform projects targeting Android (e.g. `id("com.android.library")`) are + // configured via AppPlugin or LibraryPlugin. + is KotlinBasePlugin -> + if ( + project.multiplatformExtension != null && + !project.plugins.hasPlugin(AppPlugin::class.java) && + !project.plugins.hasPlugin(LibraryPlugin::class.java) && + !project.plugins.hasPlugin(KotlinMultiplatformAndroidPlugin::class.java) + ) { + configureNonAndroidProjectForLint() + } + } + } +} + +/** Android Lint configuration entry point for Android projects. */ +private fun Project.configureAndroidProjectForLint(isLibrary: Boolean) = + extensions.findByType(LintLifecycleExtension::class.java)!!.finalizeDsl { lint -> + // The lintAnalyze task is used by `androidx-studio-integration-lint.sh`. + tasks.register("lintAnalyze") { task -> task.enabled = false } + + configureLint(lint, isLibrary) + } + +private fun Project.configureAndroidMultiplatformProjectForLint( + extension: KotlinMultiplatformAndroidLibraryTarget, + componentsExtension: KotlinMultiplatformAndroidComponentsExtension, +) { + componentsExtension.finalizeDsl { + // The lintAnalyze task is used by `androidx-studio-integration-lint.sh`. + tasks.register("lintAnalyze") { task -> task.enabled = false } + configureLint(extension.lint, isLibrary = true) + } +} + +/** Android Lint configuration entry point for non-Android projects. */ +private fun Project.configureNonAndroidProjectForLint() = afterEvaluate { + // For Android projects, the Android Gradle Plugin is responsible for applying the lint plugin; + // however, we need to apply it ourselves for non-Android projects. + apply(mapOf("plugin" to "com.android.lint")) + + // The lintAnalyzeDebug task is used by `androidx-studio-integration-lint.sh`. + tasks.register("lintAnalyzeDebug") { it.enabled = false } + + // For Android projects, we can run lint configuration last using `DslLifecycle.finalizeDsl`; + // however, we need to run it using `Project.afterEvaluate` for non-Android projects. + configureLint(project.extensions.getByType(), isLibrary = true) +} + +private fun Project.findLintProject(path: String): Project? { + return project.rootProject.findProject(path) + ?: if (allowMissingLintProject()) { + null + } else { + throw GradleException("Project $path does not exist") + } +} + +private fun Project.configureLint(lint: Lint, isLibrary: Boolean) { + val extension = project.androidXExtension + val type = extension.type.get() + val lintChecksProject = findLintProject(":lint-checks") ?: return + project.dependencies.add("lintChecks", lintChecksProject) + + if (type in setOf(SoftwareType.GRADLE_PLUGIN, SoftwareType.INTERNAL_GRADLE_PLUGIN)) { + project.rootProject.findProject(":lint:lint-gradle")?.let { + project.dependencies.add("lintChecks", it) + } + } + + // The purpose of this specific project is to test that lint is running, so + // it contains expected violations that we do not want to trigger a build failure + val isTestingLintItself = (project.path == ":lint-checks:integration-tests") + + lint.apply { + // Skip lintVital tasks on assemble. We explicitly run lintRelease for libraries. + checkReleaseBuilds = false + } + + // Lint is configured entirely in finalizeDsl so that individual projects cannot easily + // disable individual checks in the DSL for any reason. + lint.apply { + if (!isTestingLintItself) { + abortOnError = true + } + ignoreWarnings = true + + // Run lint on tests. All checks defined with test scope will be run on test sources. + // Additional checks for tests can be specified in the top-level lint.xml. + ignoreTestSources = false + checkTestSources = false + + // Write output directly to the console (and nowhere else). + textReport = true + htmlReport = false + + // Format output for convenience. + explainIssues = true + noLines = false + quiet = true + + // We run lint on each library, so we don't want transitive checking of each dependency + checkDependencies = false + + if (type.allowCallingVisibleForTestsApis) { + // Test libraries are allowed to call @VisibleForTests code + disable.add("VisibleForTests") + } else { + fatal.add("VisibleForTests") + } + + if (type.isForTesting) { + // Disable this check as we do allow usage of junit as a dependency + disable.add("InvalidPackage") + } else { + fatal.add("InvalidPackage") + } + + // Disable a check that's only relevant for apps that ship to Play Store. (b/299278101) + disable.add("ExpiredTargetSdkVersion") + + // Disable dependency checks that suggest to change them. We want libraries to be + // intentional with their dependency version bumps. + disable.add("KtxExtensionAvailable") + disable.add("GradleDependency") + + // Disable a check that's only relevant for real apps. For our test apps we're not + // concerned with drawables potentially being a little bit blurry + disable.add("IconMissingDensityFolder") + + // Disable until it works for our projects, b/171986505 + disable.add("JavaPluginLanguageLevel") + + // Explicitly disable StopShip check (see b/244617216) + disable.add("StopShip") + + // Swap the built-in RestrictedApi check for our "fixed" version (see b/297047524) + disable.add("RestrictedApi") + fatal.add("RestrictedApiAndroidX") + + // Provide stricter enforcement for project types intended to run on a device. + if (type.compilationTarget == CompilationTarget.DEVICE) { + fatal.add("Assert") + fatal.add("NewApi") + fatal.add("ObsoleteSdkInt") + fatal.add("NoHardKeywords") + fatal.add("UnusedResources") + fatal.add("KotlinPropertyAccess") + fatal.add("LambdaLast") + if (type != SoftwareType.PUBLISHED_PROTO_LIBRARY) { + // Enforce UnknownNullness for all device targeting projects except for proto + // projects that generate code without proper nullability annotations. + fatal.add("UnknownNullness") + } + + // Too many Kotlin features require synthetic accessors - we want to rely on R8 to + // remove these accessors + disable.add("SyntheticAccessor") + + // Only check for missing translations in finalized (beta and later) modules. + if (extension.mavenVersion?.isFinalApi() == true) { + fatal.add("MissingTranslation") + } else { + disable.add("MissingTranslation") + } + } else { + disable.add("BanUncheckedReflection") + disable.add("BanConcurrentHashMap") + } + + // Only show ObsoleteCompatMethod in the IDE. + disable.add("ObsoleteCompatMethod") + + // Broken in 7.0.0-alpha15 due to b/187343720 + disable.add("UnusedResources") + + if (type == SoftwareType.SAMPLES) { + // TODO: b/190833328 remove if / when AGP will analyze dependencies by default + // This is needed because SampledAnnotationDetector uses partial analysis, and + // hence requires dependencies to be analyzed. + checkDependencies = true + } + + // Only run certain checks where API tracking is important. + if (type.checkApi is RunApiTasks.No) { + disable.add("IllegalExperimentalApiUsage") + } + + // Run the JSpecifyNullness check unless opted-out (for projects that haven't migrated yet). + if (extension.optOutJSpecify) { + disable.add("JSpecifyNullness") + } else { + fatal.add("JSpecifyNullness") + } + + fatal.add("UastImplementation") // go/hide-uast-impl + fatal.add("KotlincFE10") // b/239982263 + + disable.add("RequiresWindowSdk") // temporarily disable this check due to downstream diff + + // Report errors for incompatible custom lint jars + fatal.add("ObsoleteLintCustomCheck") + + // If a project targets only Kotlin consumers, it is allowed to define experimental + // properties because the Kotlin compiler warns users that the properties are experimental. + // If a project can have Java clients, enable the lint check banning experimental properties + // because the experimental detector lint which warns Java clients about experimental usage + // isn't able to handle experimental properties correctly. + // Projects that don't run API compatibility checks can define experimental properties (lint + // check disabled) since the entire API surface makes no compatibility guarantees. + if (type.targetsKotlinConsumersOnly || !extension.shouldConfigureApiTasks().get()) { + disable.add("ExperimentalPropertyAnnotation") + } else { + fatal.add("ExperimentalPropertyAnnotation") + } + + if (!isLibrary) { + // These lint checks are specifically for libraries. + disable.add("MissingServiceExportedEqualsTrue") + disable.add("MetadataTagInsideApplicationTag") + } + + fatal.add("CheckResult") + fatal.add("PrivateResource") + + val lintXmlPath = + if (type == SoftwareType.SAMPLES) { + "buildSrc/lint/lint_samples.xml" + } else { + "buildSrc/lint/lint.xml" + } + + // Prevent libraries from fully overriding the config from buildSrc. Projects can create a + // custom lint.xml that will also be picked up by lint (which searches for one starting from + // the project dir and then moving up directories). The order of precedence for config rules + // is here: https://googlesamples.github.io/android-custom-lint-rules/usage/lintxml.md.html + if (lintConfig != null) { + throw IllegalStateException( + "Project should not override the lint configuration from `$lintXmlPath`.\n" + + "To add additional lint configuration for this project, create a `lint.xml` " + + "file in the project directory but do not set it as the `lintConfig` in the " + + "project's build file." + ) + } + + // suppress warnings more specifically than issue-wide severity (regexes) + // Currently suppresses warnings from baseline files working as intended + lintConfig = File(project.getSupportRootFolder(), lintXmlPath) + baseline = lintBaseline.get().asFile + } + project.buildOnServerDependsOnLint() +} + +private fun Project.buildOnServerDependsOnLint() { + if (!project.usingMaxDepVersions().get()) { + project.addToBuildOnServer("lint") + } +} + +private val Project.lintBaseline: RegularFileProperty + get() = project.objects.fileProperty().fileValue(File(projectDir, "lint-baseline.xml")) diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListAffectedProjectsTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListAffectedProjectsTask.kt new file mode 100644 index 0000000000000..ebcebfd0c93c8 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListAffectedProjectsTask.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.gitclient.getChangedFilesProvider +import kotlin.Suppress +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.kotlin.dsl.extra +import org.gradle.work.DisableCachingByDefault + +/** + * Determines the affected projects based on changed files. + * + * This task is designed to run every time and identifies the projects impacted by changes in the + * source files. It generates a list of Gradle task commands for the affected projects and writes + * them to an output file. + */ +@DisableCachingByDefault(because = "The purpose of this task is to run each time") +abstract class ListAffectedProjectsTask : DefaultTask() { + + @get:Input abstract val changedFiles: ListProperty + + @get:Input abstract val projectConsumersMap: MapProperty> + + @get:Input abstract val tasksToRun: ListProperty + + @get:Input abstract val shouldRunOnDependentProjects: Property + + @get:OutputFile abstract val outputFile: RegularFileProperty + + @get:Internal + val listProjectsServiceProvider: Provider = + ListProjectsService.registerOrGet(project) + + @Option( + option = "baseCommit", + description = "The base commit to compare changes against. Defaults to last merge commit.", + ) + fun setBaseCommit(commit: String?) { + changedFiles.set(project.getChangedFilesProvider(project.provider { commit })) + } + + @Suppress("UNUSED") + @Option( + option = "tasksToRun", + description = "Comma-separated list of tasks to run (e.g. 'bOS, allHostTests')", + ) + fun setTasksRun(tasks: String) { + tasksToRun.set(tasks.split(",").map(String::trim)) + } + + @Suppress("UNUSED") + @Option( + option = "runOnDependentProjects", + description = "Boolean flag to also run tasks on dependent projects", + ) + fun setRunOnDependentProjects(flag: String) { + this.shouldRunOnDependentProjects.set(flag.toBoolean()) + } + + @TaskAction + fun listAffectedProjects() { + val changedFilesList = changedFiles.get() + println("Changed files: $changedFilesList") + val allProjects = listProjectsServiceProvider.get().allPossibleProjects + val projectConsumers = projectConsumersMap.get() + val tasks = tasksToRun.get() + check(tasks.isNotEmpty()) { "tasksToRun cannot be empty" } + + val projectByFilePath = allProjects.associateBy({ it.filePath }, { it.gradlePath }) + + val changedProjects = + changedFilesList + .mapNotNull { changedFile -> + when { + changedFile.startsWith("buildSrc/") -> ":buildSrc-tests" + "/src/" in changedFile -> { + val candidate = changedFile.substringBefore("/src/") + projectByFilePath[candidate] + } + else -> { + val sortedProjects = + allProjects + .map { it.filePath to it.gradlePath } + .sortedByDescending { it.first.length } + sortedProjects + .firstOrNull { (projectFilePath, _) -> + changedFile.startsWith(projectFilePath) + } + ?.second + } + } + } + .toSet() + + val affectedProjects = + if (shouldRunOnDependentProjects.get()) { + changedProjects.flatMap { findAllProjectsDependingOn(it, projectConsumers) }.toSet() + } else { + changedProjects + } + + val commands = + affectedProjects + // TODO(b/396611615): Remove when :docs-tip-of-tree can run bOS locally + .filterNot { it == ":docs-tip-of-tree" } + .flatMap { project -> tasks.map { task -> "$project:$task" } } + + with(outputFile.get().asFile) { + parentFile.mkdirs() + writeText(commands.joinToString(" ")) + } + } +} + +private fun findAllProjectsDependingOn( + projectPath: String, + projectConsumers: Map>, +): Set { + val result = mutableSetOf() + val toBeTraversed = ArrayDeque().apply { add(projectPath) } + + while (toBeTraversed.isNotEmpty()) { + val path = toBeTraversed.removeFirst() + if (result.add(path)) { + projectConsumers[path]?.let { dependents -> toBeTraversed.addAll(dependents) } + } + } + return result +} + +internal fun Project.registerListAffectedProjectsTask() = + tasks.register("listAffectedProjects", ListAffectedProjectsTask::class.java) { task -> + task.tasksToRun.convention(listOf("bOS")) + task.shouldRunOnDependentProjects.convention(false) + task.setBaseCommit(null) + + @Suppress("UNCHECKED_CAST") + task.projectConsumersMap.set( + (gradle.extra["allProjectConsumers"] as Map>) + ) + + task.outputFile.set(layout.buildDirectory.file("changedProjects.txt")) + + // Always run task + task.outputs.upToDateWhen { false } + } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListAndroidXPropertiesTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListAndroidXPropertiesTask.kt new file mode 100644 index 0000000000000..330ddecfeda3b --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListAndroidXPropertiesTask.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** Lists recognized properties whose names start with "androidx" */ +@DisableCachingByDefault(because = "Too many inputs to cache, and runs quickly anyway") +abstract class ListAndroidXPropertiesTask : DefaultTask() { + init { + group = "Help" + description = "Lists AndroidX-specific properties (specifiable via -Pandroidx.*)" + } + + @TaskAction + fun exec() { + project.logger.lifecycle(ALL_ANDROIDX_PROPERTIES.joinToString("\n")) + project.logger.lifecycle("See AndroidXGradleProperties.kt for more information") + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListProjectsService.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListProjectsService.kt new file mode 100644 index 0000000000000..e2a1cf3107586 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListProjectsService.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +/** Lists projects as specified by settings.gradle */ +abstract class ListProjectsService : BuildService { + interface Parameters : BuildServiceParameters { + var settingsFile: Provider + } + + // Lists all project paths mentioned in frameworks/support/settings.gradle + // Note that this might be more than the full list of projects configured in this build: + // a) Configuration-on-demand can disable projects mentioned in settings.gradle + // B) Playground builds use their own settings.gradle files + val allPossibleProjects: List by lazy { + SettingsParser.findProjects(parameters.settingsFile.get()) + } + + companion object { + internal fun registerOrGet(project: Project): Provider { + // service that can compute full list of projects in settings.gradle + val settings = project.lazyReadFile("settings.gradle") + return project.gradle.sharedServices.registerIfAbsent( + "listProjectsService", + ListProjectsService::class.java, + ) { spec -> + spec.parameters.settingsFile = settings + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt new file mode 100644 index 0000000000000..b54bef5102889 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Task +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +/** Finds the outputs of every task and saves this mapping into a file */ +@CacheableTask +abstract class ListTaskOutputsTask : DefaultTask() { + @OutputFile val outputFile: RegularFileProperty = project.objects.fileProperty() + @Input val removePrefixes: MutableList = mutableListOf() + @get:Nested abstract val producers: ListProperty + + init { + group = "Help" + project.gradle.taskGraph.whenReady { + val taskOutputProducerList = mutableListOf() + project.allprojects { otherProject -> + otherProject.tasks.forEach { task -> + project.objects.newInstance(TaskOutputProducer::class.java).apply { + taskPath.set(task.path) + taskClass.set(task::class.qualifiedName ?: task::class.java.name) + validate.set(shouldValidateTaskOutput(task)) + val fileElements = task.outputs.files.elements + outputPaths.set( + fileElements.map { set -> + set.map { it.asFile.invariantSeparatorsPath } + } + ) + taskOutputProducerList.add(this) + } + } + } + producers.set(taskOutputProducerList) + } + } + + fun removePrefix(prefix: String) { + removePrefixes.add("$prefix/") + } + + @TaskAction + fun exec() { + val outputText = computeOutputText(producers.get()) + val outputFile = outputFile.get() + outputFile.asFile.writeText(outputText) + } + + private fun computeOutputText(producers: List): String { + val tasksByOutput: MutableMap = hashMapOf() + for (producer in producers) { + for (path in producer.outputPaths.get()) { + val existing = tasksByOutput[path] + if (existing != null) { + if (existing.validate.get() && producer.validate.get()) { + throw GradleException( + "Output file $path was declared as an output of multiple tasks: " + + "${producer.taskPath.get()} and ${existing.taskPath.get()}" + ) + } + if (existing.taskPath.get() > producer.taskPath.get()) continue + } + tasksByOutput[path] = producer + } + } + return formatTasks(tasksByOutput, removePrefixes) + } + + // Given a map from output file path to Task, formats into a String + private fun formatTasks( + tasksByOutput: MutableMap, + removePrefixes: List, + ): String { + val messages: MutableList = mutableListOf() + for ((path, task) in tasksByOutput) { + var filePath = path + for (prefix in removePrefixes) { + filePath = filePath.removePrefix(prefix) + } + + messages.add( + formatInColumns( + listOf( + filePath, + " - " + task.taskPath.get() + " (" + task.taskClass.get() + ")", + ) + ) + ) + } + messages.sort() + return messages.joinToString("\n") + } + + // Given a list of columns, indents and joins them to be easy to read + private fun formatInColumns(columns: List): String { + val components = mutableListOf() + var textLength = 0 + for (column in columns) { + val roundedTextLength = + if (textLength == 0) { + textLength + } else { + ((textLength / 32) + 1) * 32 + } + val extraSpaces = " ".repeat(roundedTextLength - textLength) + components.add(extraSpaces) + textLength = roundedTextLength + components.add(column) + textLength += column.length + } + return components.joinToString("") + } +} + +// TODO(149103692): remove all elements of this set +private val taskNamesKnownToDuplicateOutputs = + setOf( + // Instead of adding new elements to this set, prefer to disable unused tasks when possible + + // b/308798582 + "transformNonJvmMainCInteropDependenciesMetadataForIde", + "transformAndroidNativeMainCInteropDependenciesMetadataForIde", + "transformAndroidNativeTestCInteropDependenciesMetadataForIde", + "transformAppleMainCInteropDependenciesMetadataForIde", + "transformAppleTestCInteropDependenciesMetadataForIde", + "transformDarwinTestCInteropDependenciesMetadataForIde", + "transformDarwinMainCInteropDependenciesMetadataForIde", + "transformCommonMainCInteropDependenciesMetadataForIde", + "transformCommonTestCInteropDependenciesMetadataForIde", + "transformIosMainCInteropDependenciesMetadataForIde", + "transformIosTestCInteropDependenciesMetadataForIde", + "transformMacosMainCInteropDependenciesMetadataForIde", + "transformMacosTestCInteropDependenciesMetadataForIde", + "transformNativeTestCInteropDependenciesMetadataForIde", + "transformNativeMainCInteropDependenciesMetadataForIde", + "transformTvosMainCInteropDependenciesMetadataForIde", + "transformTvosTestCInteropDependenciesMetadataForIde", + "transformWatchosMainCInteropDependenciesMetadataForIde", + "transformWatchosTestCInteropDependenciesMetadataForIde", + "transformUnixMainCInteropDependenciesMetadataForIde", + "transformUnixTestCInteropDependenciesMetadataForIde", + "transformLinuxMainCInteropDependenciesMetadataForIde", + "transformLinuxTestCInteropDependenciesMetadataForIde", + "transformNonIosNativeTestCInteropDependenciesMetadataForIde", + "transformNonJvmCommonMainCInteropDependenciesMetadataForIde", + + // The following tests intentionally have the same output of golden images + "updateGoldenDesktopTest", + "updateGoldenDebugUnitTest", + + // The following tasks have the same output file: + // ../../prebuilts/androidx/javascript-for-kotlin/yarn.lock + "kotlinRestoreYarnLock", + "kotlinWasmRestoreYarnLock", + "kotlinNpmInstall", + "kotlinWasmNpmInstall", + "kotlinUpgradePackageLock", + "kotlinWasmUpgradePackageLock", + "kotlinUpgradeYarnLock", + "kotlinWasmUpgradeYarnLock", + "kotlinStorePackageLock", + "kotlinWasmStorePackageLock", + "kotlinStoreYarnLock", + "kotlinWasmStoreYarnLock", + + // The following tasks have the same output file: + // $OUT_DIR/androidx/build/wasm/yarn.lock + "wasmKotlinRestoreYarnLock", + "wasmKotlinNpmInstall", + "wasmKotlinUpgradePackageLock", + "wasmKotlinStorePackageLock", + "wasmKotlinUpgradeYarnLock", + "wasmKotlinStoreYarnLock", + + // The following tasks have the same output configFile file: + // projectBuildDir/js/packages/projectName-wasm-js/webpack.config.js + // Remove when https://youtrack.jetbrains.com/issue/KT-70029 / b/361319689 is resolved + // and set configFile location for each task + "wasmJsBrowserDevelopmentWebpack", + "wasmJsBrowserDevelopmentRun", + "wasmJsBrowserProductionWebpack", + "wasmJsBrowserProductionRun", + "jsTestTestDevelopmentExecutableCompileSync", + + // https://youtrack.jetbrains.com/issue/KT-79936 + // $OUT_DIR/.gradle/nodejs/node-v22.13.0-darwin-arm64.hash + "kotlinNodeJsSetup", + "kotlinWasmNodeJsSetup", + // $OUT_DIR/.gradle/yarn/yarn-v1.22.17.hash + "wasmKotlinYarnSetup", + "kotlinYarnSetup", + + // $OUT_DIR/.gradle/binaryen/binaryen-version_122.hash + "kotlinBinaryenSetup", + "kotlinWasmBinaryenSetup", + ) + +fun shouldValidateTaskOutput(task: Task): Boolean { + if (!task.enabled) { + return false + } + return !taskNamesKnownToDuplicateOutputs.contains(task.name) +} + +/** Nested input describing each projects tasks and its outputs */ +abstract class TaskOutputProducer { + + @get:Input abstract val taskPath: Property + + @get:Input abstract val taskClass: Property + + @get:Input abstract val validate: Property + + /** + * A collection of output paths from various tasks. + * + * This property intentionally avoids using a [org.gradle.api.file.FileCollection] to prevent + * creating a direct task dependency between the producer tasks and the [ListTaskOutputsTask]. + * By storing the paths as strings, we can inspect the output locations without coupling the + * tasks in the execution graph. + */ + @get:Input abstract val outputPaths: ListProperty +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt new file mode 100644 index 0000000000000..cc62a8188eb3b --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt @@ -0,0 +1,577 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.ProjectLayoutType.Companion.isJetBrainsFork +import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask +import androidx.build.sources.PublishingVariant +import com.android.build.api.dsl.LibraryExtension +import com.android.build.gradle.AppPlugin +import com.android.build.gradle.LibraryPlugin +import com.android.utils.childrenIterator +import com.android.utils.forEach +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import com.google.gson.stream.JsonWriter +import java.io.StringWriter +import org.dom4j.Element +import org.dom4j.io.XMLWriter +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.XmlProvider +import org.gradle.api.component.SoftwareComponent +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.provider.Provider +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPom +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.tasks.GenerateMavenPom +import org.gradle.api.publish.tasks.GenerateModuleMetadata +import org.gradle.api.tasks.bundling.Zip +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.findByType +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.androidx.build.JetBrainsPublication +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper + +fun Project.configureMavenArtifactUpload( + androidXExtension: AndroidXExtension, + androidXKmpExtension: AndroidXMultiplatformExtension, + afterConfigure: () -> Unit, +) { + if (isJetBrainsFork(project) && JetBrainsPublication.shouldPublish(this)) return + apply(mapOf("plugin" to "maven-publish")) + var registered = false + fun registerOnFirstPublishableArtifact(component: SoftwareComponent) { + if (!registered) { + configureComponentPublishing( + androidXExtension, + androidXKmpExtension, + component, + afterConfigure, + ) + Release.register(this, androidXExtension) + registered = true + } + } + afterEvaluate { + if (!androidXExtension.shouldPublish.get()) { + return@afterEvaluate + } + components.configureEach { component -> + if (isValidReleaseComponent(component)) { + registerOnFirstPublishableArtifact(component) + } + } + } + // validate that all libraries that should be published actually get tasks registered. + // named() will throw UnknownTaskException if the task is not registered. + gradle.taskGraph.whenReady { graph -> + if (releaseTaskShouldBeRegistered(androidXExtension)) { + tasks.named(Release.PROJECT_ARCHIVE_ZIP_TASK_NAME) + } + if (buildInfoTaskShouldBeRegistered(androidXExtension)) { + if (!androidXExtension.isIsolatedProjectsEnabled()) { + tasks.named(CreateLibraryBuildInfoFileTask.TASK_NAME) + } + } + } +} + +private fun Project.releaseTaskShouldBeRegistered(extension: AndroidXExtension): Boolean { + if (plugins.hasPlugin(AppPlugin::class.java)) { + return false + } + if (!extension.shouldRelease.get() && !isSnapshotBuild()) { + return false + } + return extension.shouldPublish.get() +} + +private fun Project.buildInfoTaskShouldBeRegistered(extension: AndroidXExtension): Boolean { + if (plugins.hasPlugin(AppPlugin::class.java)) { + return false + } + return extension.shouldRelease.get() +} + +/** Configure publishing for a [SoftwareComponent]. */ +private fun Project.configureComponentPublishing( + extension: AndroidXExtension, + androidxKmpExtension: AndroidXMultiplatformExtension, + component: SoftwareComponent, + afterConfigure: () -> Unit, +) { + val androidxGroup = validateCoordinatesAndGetGroup(extension) + group = androidxGroup.group + + /* + * Provides a set of maven coordinates (groupId:artifactId) of artifacts in AndroidX + * that are Android Libraries. + */ + val androidLibrariesSetProvider: Provider> = provider { + val androidxAndroidProjects = mutableSetOf() + // Check every project is the project map to see if they are an Android Library + val projectModules = extension.mavenCoordinatesToProjectPathMap + for ((mavenCoordinates, projectPath) in projectModules) { + project.findProject(projectPath)?.let { project -> + if (project.plugins.hasPlugin(LibraryPlugin::class.java)) { + androidxAndroidProjects.add(mavenCoordinates) + } + if (project.hasAndroidMultiplatformPlugin()) { + androidxAndroidProjects.add("$mavenCoordinates-android") + } + } + } + androidxAndroidProjects + } + + configure { + repositories { + it.maven { repo -> repo.setUrl(getRepositoryDirectory()) } + it.maven { repo -> repo.setUrl(getPerProjectRepositoryDirectory()) } + } + publications { + if (appliesJavaGradlePluginPlugin()) { + // The 'java-gradle-plugin' will also add to the 'pluginMaven' publication + it.create("pluginMaven") + afterConfigure() + } else { + if (project.isMultiplatformPublicationEnabled()) { + afterConfigure() + } else { + it.create("maven") { from(component) } + afterConfigure() + } + } + } + publications.withType(MavenPublication::class.java).configureEach { publication -> + // Used to add buildId to Gradle module metadata set below + publication.withBuildIdentifier() + val isKmpAnchor = (publication.name == KMP_ANCHOR_PUBLICATION_NAME) + val pomPlatform = androidxKmpExtension.defaultPlatform + // b/297355397 If a kmp project has Android as the default platform, there might + // externally be legacy projects depending on its .pom + // We advertise a stub .aar in this .pom for backwards compatibility and + // add a dependency on the actual .aar + val addStubAar = isKmpAnchor && pomPlatform == PlatformIdentifier.ANDROID.id + val buildDir = project.layout.buildDirectory + if (addStubAar) { + val minSdk = + project.extensions.findByType()?.defaultConfig?.minSdk + ?: extensions + .findByType() + ?.agpKmpExtension + ?.minSdk + ?: throw GradleException( + "Couldn't find valid Android extension to read minSdk from" + ) + // create a unique namespace for this .aar, different from the android artifact + val stubNamespace = + project.group.toString().replace(':', '.') + + "." + + project.name.replace('-', '.') + + ".anchor" + val unpackedStubAarTask = + tasks.register("unpackedStubAar", UnpackedStubAarTask::class.java) { aarTask -> + aarTask.aarPackage.set(stubNamespace) + aarTask.minSdkVersion.set(minSdk) + aarTask.outputDir.set(buildDir.dir("intermediates/stub-aar")) + } + val stubAarTask = + tasks.register("stubAar", ZipStubAarTask::class.java) { zipTask -> + zipTask.from(unpackedStubAarTask.flatMap { it.outputDir }) + zipTask.destinationDirectory.set(buildDir.dir("outputs")) + zipTask.archiveExtension.set("aar") + } + publication.artifact(stubAarTask) + } + + publication.pom { pom -> + if (addStubAar) { + pom.packaging = "aar" + } + addInformativeMetadata(extension, pom) + tweakDependenciesMetadata( + androidxGroup, + pom, + androidLibrariesSetProvider, + isKmpAnchor, + pomPlatform, + ) + } + } + } + + // Workarounds for https://github.com/gradle/gradle/issues/20011 + project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task -> + task.doLast { + val metadataFile = task.outputFile.asFile.get() + val metadata = metadataFile.readText() + verifyGradleMetadata(metadata) + val sortedMetadata = sortGradleMetadataDependencies(metadata) + + if (metadata != sortedMetadata) { + metadataFile.writeText(sortedMetadata) + } + } + } + project.tasks.withType(GenerateMavenPom::class.java).configureEach { task -> + task.doLast { + val pomFile = task.destination + val pom = pomFile.readText() + val sortedPom = sortPomDependencies(pom) + + if (pom != sortedPom) { + pomFile.writeText(sortedPom) + } + } + } + + val buildIdProvider = project.providers.getBuildId() + // Workaround for https://github.com/gradle/gradle/issues/31218 + project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task -> + task.doLast { + if (buildIdProvider.isPresent) { + val buildId = buildIdProvider.get() + val metadata = task.outputFile.asFile.get() + val text = metadata.readText() + metadata.writeText( + text.replace("\"buildId\": .*".toRegex(), "\"buildId:\": \"${buildId}\"") + ) + } + } + } +} + +/** Looks for a dependencies XML element within [pom] and sorts its contents. */ +fun sortPomDependencies(pom: String): String { + // Workaround for using the default namespace in dom4j. + val namespaceUris = mapOf("ns" to "http://maven.apache.org/POM/4.0.0") + val document = parseXml(pom, namespaceUris) + + // For each element, sort the contained elements in-place. + document.rootElement.selectNodes("ns:dependencies").filterIsInstance().forEach { + element -> + val deps = element.elements() + val sortedDeps = deps.toSortedSet(compareBy { it.stringValue }).toList() + // Content contains formatting nodes, so to avoid modifying those we replace + // each element with the sorted element from its respective index. Note this + // will not move adjacent elements, so any comments would remain in their + // original order. + element.content().replaceAll { + val index = sortedDeps.indexOf(it) + if (index >= 0) { + deps[index] + } else { + it + } + } + } + + // Write to string. Note that this does not preserve the original indent level, but it + // does preserve line breaks -- not that any of this matters for client XML parsing. + val stringWriter = StringWriter() + XMLWriter(stringWriter).apply { + setIndentLevel(2) + write(document) + close() + } + + return stringWriter.toString() +} + +/** Looks for a dependencies JSON element within [metadata] and sorts its contents. */ +fun sortGradleMetadataDependencies(metadata: String): String { + val gson = GsonBuilder().create() + val jsonObj = gson.fromJson(metadata, JsonObject::class.java)!! + jsonObj.getAsJsonArray("variants").forEach { entry -> + (entry as? JsonObject)?.getAsJsonArray("dependencies")?.let { jsonArray -> + val sortedSet = jsonArray.toSortedSet(compareBy { it.toString() }) + jsonArray.removeAll { true } + sortedSet.forEach { element -> jsonArray.add(element) } + } + } + + val stringWriter = StringWriter() + val jsonWriter = JsonWriter(stringWriter) + jsonWriter.setIndent(" ") + gson.toJson(jsonObj, jsonWriter) + return stringWriter.toString() +} + +/** + * Checks the variants field in the metadata file has an entry containing "sourcesElements". All our + * publications must be published with a sources variant. + */ +fun verifyGradleMetadata(metadata: String) { + val gson = GsonBuilder().create() + val jsonObj = gson.fromJson(metadata, JsonObject::class.java)!! + jsonObj.getAsJsonArray("variants").firstOrNull { variantElement -> + variantElement.asJsonObject + .get("name") + .asString + .contains(other = PublishingVariant.SourcesElements.name, ignoreCase = true) + } + ?: throw Exception( + "The ${PublishingVariant.SourcesElements.name} variant must exist in the module file." + ) +} + +private fun Project.isMultiplatformPublicationEnabled(): Boolean { + return extensions.findByType() != null +} + +private fun Project.isValidReleaseComponent(component: SoftwareComponent) = + component.name == releaseComponentName() + +private fun Project.releaseComponentName() = + when { + plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java) -> "kotlin" + plugins.hasPlugin(JavaPlugin::class.java) -> "java" + else -> "release" + } + +private fun Project.validateCoordinatesAndGetGroup(extension: AndroidXExtension): LibraryGroup { + val mavenGroup = extension.mavenGroup + if (mavenGroup == null) { + val groupExplanation = extension.explainMavenGroup().joinToString("\n") + throw Exception("You must specify mavenGroup for $path :\n$groupExplanation") + } + val strippedGroupId = mavenGroup.group.substringAfterLast(".") + if ( + !extension.bypassCoordinateValidation && + mavenGroup.group.startsWith("androidx") && + !name.startsWith(strippedGroupId) + ) { + throw Exception("Your artifactId must start with '$strippedGroupId'. (currently is $name)") + } + return mavenGroup +} + +private fun Project.addInformativeMetadata(extension: AndroidXExtension, pom: MavenPom) { + pom.name.set(extension.name) + pom.description.set(extension.description) + pom.url.set( + provider { + fun defaultUrl() = + "https://developer.android.com/jetpack/androidx/releases/" + + extension.mavenGroup!!.group.removePrefix("androidx.").replace(".", "-") + + "#" + + extension.project.version() + getAlternativeProjectUrl() ?: defaultUrl() + } + ) + pom.inceptionYear.set(extension.inceptionYear) + pom.licenses { licenses -> + licenses.license { license -> + license.name.set(extension.license.name) + license.url.set(extension.license.url) + license.distribution.set("repo") + } + + for (extraLicense in extension.getExtraLicenses()) { + licenses.license { license -> + license.name.set(provider { extraLicense.name!! }) + license.url.set(provider { extraLicense.url!! }) + license.distribution.set("repo") + } + } + } + pom.scm { scm -> + scm.url.set("https://cs.android.com/androidx/platform/frameworks/support") + scm.connection.set(ANDROID_GIT_URL) + } + pom.organization { org -> org.name.set("The Android Open Source Project") } + pom.developers { devs -> + devs.developer { dev -> dev.name.set("The Android Open Source Project") } + } +} + +private fun tweakDependenciesMetadata( + mavenGroup: LibraryGroup, + pom: MavenPom, + androidLibrariesSetProvider: Provider>, + kmpAnchor: Boolean, + pomPlatform: String?, +) { + pom.withXml { xml -> + // The following code depends on getProjectsMap which is only available late in + // configuration at which point Java Library plugin's variants are not allowed to be + // modified. TODO remove the use of getProjectsMap and move to earlier configuration. + // For more context see: + // https://android-review.googlesource.com/c/platform/frameworks/support/+/1144664/8/buildSrc/src/main/kotlin/androidx/build/MavenUploadHelper.kt#177 + assignSingleVersionDependenciesInGroupForPom(xml, mavenGroup) + assignAarDependencyTypes(xml, androidLibrariesSetProvider.get()) + ensureConsistentJvmSuffix(xml) + + if (kmpAnchor && pomPlatform != null) { + insertDefaultMultiplatformDependencies(xml, pomPlatform) + } + } +} + +// TODO(aurimas): remove this when Gradle bug is fixed. +// https://github.com/gradle/gradle/issues/3170 +fun assignAarDependencyTypes(xml: XmlProvider, androidLibrariesSet: Set) { + val xmlElement = xml.asElement() + val dependencies = xmlElement.find { it.nodeName == "dependencies" } as? org.w3c.dom.Element + + dependencies?.getElementsByTagName("dependency")?.forEach { dependency -> + val groupId = + dependency.find { it.nodeName == "groupId" }?.textContent + ?: throw IllegalArgumentException("Failed to locate groupId node") + val artifactId = + dependency.find { it.nodeName == "artifactId" }?.textContent + ?: throw IllegalArgumentException("Failed to locate artifactId node") + if (androidLibrariesSet.contains("$groupId:$artifactId")) { + dependency.appendElement("type", "aar") + } + } +} + +fun insertDefaultMultiplatformDependencies(xml: XmlProvider, platformId: String) { + val xmlElement = xml.asElement() + val groupId = + xmlElement.find { it.nodeName == "groupId" }?.textContent + ?: throw IllegalArgumentException("Failed to locate groupId node") + val artifactId = + xmlElement.find { it.nodeName == "artifactId" }?.textContent + ?: throw IllegalArgumentException("Failed to locate artifactId node") + val version = + xmlElement.find { it.nodeName == "version" }?.textContent + ?: throw IllegalArgumentException("Failed to locate version node") + + // Find the top-level element or add one if there are no other dependencies. + val dependencies = + xmlElement.find { it.nodeName == "dependencies" } + ?: xmlElement.appendElement("dependencies") + dependencies.appendElement("dependency").apply { + appendElement("groupId", groupId) + appendElement("artifactId", "$artifactId-$platformId") + appendElement("version", version) + if (platformId == PlatformIdentifier.ANDROID.id) { + appendElement("type", "aar") + } + appendElement("scope", "compile") + } +} + +private fun org.w3c.dom.Node.appendElement( + tagName: String, + textValue: String? = null, +): org.w3c.dom.Element { + val element = ownerDocument.createElement(tagName) + appendChild(element) + + if (textValue != null) { + val textNode = ownerDocument.createTextNode(textValue) + element.appendChild(textNode) + } + + return element +} + +private fun org.w3c.dom.Node.find(predicate: (org.w3c.dom.Node) -> Boolean): org.w3c.dom.Node? { + val iterator = childrenIterator() + while (iterator.hasNext()) { + val node = iterator.next() + if (predicate(node)) { + return node + } + } + return null +} + +/** + * Modifies the given .pom to specify that every dependency in refers to a single version + * and can't be automatically promoted to a new version. This will replace, for example, a version + * string of "1.0" with a version string of "[1.0]" + * + * Note: this is not enforced in Gradle nor in plain Maven (without the Enforcer plugin) + * (https://github.com/gradle/gradle/issues/8297) + */ +fun assignSingleVersionDependenciesInGroupForPom(xml: XmlProvider, mavenGroup: LibraryGroup) { + if (!mavenGroup.requireSameVersion) { + return + } + + val dependencies = + xml.asElement().find { it.nodeName == "dependencies" } as? org.w3c.dom.Element ?: return + + dependencies.getElementsByTagName("dependency").forEach { dependency -> + val groupId = + dependency.find { it.nodeName == "groupId" }?.textContent + ?: throw IllegalArgumentException("Failed to locate groupId node") + if (groupId == mavenGroup.group) { + val versionNode = + dependency.find { it.nodeName == "version" } + ?: throw IllegalArgumentException("Failed to locate version node") + val version = versionNode.textContent + if (isVersionRange(version)) { + throw GradleException("Unsupported version '$version': already is a version range") + } + val pinnedVersion = "[$version]" + versionNode.textContent = pinnedVersion + } + } +} + +private fun isVersionRange(text: String): Boolean { + return text.contains("[") || + text.contains("]") || + text.contains("(") || + text.contains(")") || + text.contains(",") +} + +/** + * Ensures that artifactIds are consistent when using configuration caching. A workaround for + * https://github.com/gradle/gradle/issues/18369 + */ +fun ensureConsistentJvmSuffix(xml: XmlProvider) { + val dependencies = + xml.asElement().find { it.nodeName == "dependencies" } as? org.w3c.dom.Element ?: return + + dependencies.getElementsByTagName("dependency").forEach { dependency -> + val artifactId = + dependency.find { it.nodeName == "artifactId" } + ?: throw IllegalArgumentException("Failed to locate artifactId node") + // kotlinx-coroutines-core is only a .pom and only depends on kotlinx-coroutines-core-jvm, + // so the two artifacts should be approximately equivalent. However, + // when loading from configuration cache, Gradle often returns a different resolution. + // We replace it here to ensure consistency and predictability, and + // to avoid having to rerun any zip tasks that include it + if (artifactId.textContent == "kotlinx-coroutines-core-jvm") { + artifactId.textContent = "kotlinx-coroutines-core" + } + } +} + +private fun Project.appliesJavaGradlePluginPlugin() = pluginManager.hasPlugin("java-gradle-plugin") + +private const val ANDROID_GIT_URL = + "scm:git:https://android.googlesource.com/platform/frameworks/support" + +// Name of KMP root publication +// https://github.com/JetBrains/kotlin/blob/bf6cb00fa8db7879c323bad863f58a0545c3d655/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/publishing/Publishing.kt#L54 +internal const val KMP_ANCHOR_PUBLICATION_NAME = "kotlinMultiplatform" + +@DisableCachingByDefault(because = "Not worth caching") +internal abstract class ZipStubAarTask : Zip() diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/MaxDepVersions.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/MaxDepVersions.kt new file mode 100644 index 0000000000000..e977fcfba03c9 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/MaxDepVersions.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Project +import org.gradle.api.artifacts.component.ModuleComponentSelector + +/** + * If useMaxDepVersions is set, iterate through all the dependencies and substitute any androidx + * artifact dependency with the local tip of tree version of the library. + */ +internal fun Project.configureMaxDepVersions(extension: AndroidXExtension) { + if (!usingMaxDepVersions().get()) return + val projectModules = extension.mavenCoordinatesToProjectPathMap + configurations.configureEach { configuration -> + configuration.resolutionStrategy.dependencySubstitution.apply { + all { dep -> + val requested = dep.requested + if (requested is ModuleComponentSelector) { + val module = requested.group + ":" + requested.module + if (projectModules.containsKey(module)) { + dep.useTarget(project(projectModules[module]!!)) + } + } + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/PrintProjectCoordinatesTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/PrintProjectCoordinatesTask.kt new file mode 100644 index 0000000000000..83d9d18feabc5 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/PrintProjectCoordinatesTask.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +// This task prints the coordinates (group/artifact/version) of a project +@DisableCachingByDefault(because = "The purpose of this task is to print information") +abstract class PrintProjectCoordinatesTask : DefaultTask() { + + fun configureWithAndroidXExtension(androidXExtension: AndroidXExtension) { + projectGroup = androidXExtension.mavenGroup + groupExplanation = androidXExtension.explainMavenGroup() + projectName = project.name + version = project.version.toString() + projectDir = project.projectDir.relativeTo(project.rootDir) + projectPath = project.path + } + + @Internal // Task is always out-of-date: no need to track inputs + var projectGroup: LibraryGroup? = null + + @Internal // Task is always out-of-date: no need to track inputs + var groupExplanation: List? = null + + @Internal // Task is always out-of-date: no need to track inputs + var projectName: String? = null + + @Internal // Task is always out-of-date: no need to track inputs + var version: String? = null + + @Internal // Task is always out-of-date: no need to track inputs + var projectDir: File? = null + + @Internal // Task is always out-of-date: no need to track inputs + var projectPath: String? = null + + @TaskAction + fun printInformation() { + val projectGroup = projectGroup + val versionFrom = + if (projectGroup?.atomicGroupVersion == null) { + "build.gradle: mavenVersion" + } else { + "group.atomicGroupVersion" + } + + val groupExplanation = groupExplanation!! + val lines = + mutableListOf(listOf("filepath: $projectDir/build.gradle ", "(from settings.gradle)")) + // put each component of the explanation on its own line + groupExplanation.forEachIndexed { i, component -> + if (i == 0) lines.add(listOf("group : ${projectGroup?.group} ", component)) + else lines.add(listOf("", component)) + } + lines.add(listOf("artifact: $projectName ", "(from project name)")) + lines.add(listOf("version : $version ", "(from $versionFrom)")) + printTable(lines) + } + + private fun printTable(lines: List>) { + val columnSizes = getColumnSizes(lines) + for (line in lines) { + println(formatRow(line, columnSizes)) + } + } + + private fun formatRow(line: List, columnSizes: List): String { + var result = "" + for (i in line.indices) { + val word = line[i] + val columnSize = columnSizes[i] + // only have to pad columns before the last column + result += if (i != line.size - 1) word.padEnd(columnSize) else word + } + return result + } + + private fun getColumnSizes(lines: List>): List { + val maxLengths = mutableListOf() + for (line in lines) { + for (i in line.indices) { + val word = line[i] + if (maxLengths.size <= i) maxLengths.add(0) + if (maxLengths[i] < word.length) maxLengths[i] = word.length + } + } + return maxLengths + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProguardConfiguration.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProguardConfiguration.kt new file mode 100644 index 0000000000000..9d1cc0ba76ea6 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProguardConfiguration.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import com.android.build.api.dsl.ConsumerKeepRules +import com.android.build.api.dsl.LibraryDefaultConfig +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** + * Add a blank consumer proguard rules file to the JAR if the library has not set up an explicit set + * of rules. + */ +internal fun Project.setUpBlankProguardFileForJarIfNeeded(javaExtension: JavaPluginExtension) { + if (project.multiplatformExtension != null) return // skip KMP projects + val mainSources = javaExtension.sourceSets.getByName("main") + val provider = + tasks.register("emptyProguardFileCopy", BlankProguardFileGenerator::class.java) { + it.blankProguardFile.set(blankProguardRules()) + it.outputDirectory.set(layout.buildDirectory.dir("blankProguard")) + it.nonGeneratedResources.from(mainSources.resources.sourceDirectories) + // unique name like "androidx-arch-core-core-common" + it.libraryName.set("androidx${project.path.replace(":", "-")}") + } + mainSources.output.dir(provider.flatMap { it.outputDirectory }) +} + +/** + * Add a blank consumer proguard rules file to the AAR if the library has not set up an explicit set + * of rules. + */ +internal fun Project.setUpBlankProguardFileForAarIfNeeded(config: LibraryDefaultConfig) { + if (config.consumerProguardFiles.isEmpty()) { + config.consumerProguardFiles.add(blankProguardRules()) + } +} + +/** + * Add a blank consumer proguard rules file to the AAR if the library has not set up an explicit set + * of rules. + */ +@Suppress("UnstableApiUsage") // b/393137152 +internal fun Project.setUpBlankProguardFileForKmpAarIfNeeded(consumerKeepRules: ConsumerKeepRules) { + if (consumerKeepRules.files.isEmpty()) { + consumerKeepRules.publish = true + consumerKeepRules.files.add(blankProguardRules()) + } +} + +@DisableCachingByDefault +abstract class BlankProguardFileGenerator : DefaultTask() { + @get:[InputFile PathSensitive(PathSensitivity.NONE)] + abstract val blankProguardFile: RegularFileProperty + + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val nonGeneratedResources: ConfigurableFileCollection + + @get:Input abstract val libraryName: Property + + @get:OutputDirectory abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun copyEmptyFile() { + outputDirectory.get().asFile.deleteRecursively() + val hasExplicitProguardFile = + nonGeneratedResources.any { File(it, "META-INF/proguard").exists() } + // Check if the library already contains explicit proguard file + if (hasExplicitProguardFile) return + blankProguardFile + .get() + .asFile + .copyTo( + File(outputDirectory.get().asFile, "META-INF/proguard/${libraryName.get()}.pro") + ) + } +} + +private fun Project.blankProguardRules(): File = + project.getSupportRootFolder().resolve("buildSrc/blank-proguard-rules/proguard-rules.pro") diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectConfigValidators.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectConfigValidators.kt new file mode 100644 index 0000000000000..b2b815e4660e5 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectConfigValidators.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.io.File +import org.gradle.api.GradleException +import org.gradle.api.Project + +/** Validates the project's Maven group against Jetpack guidelines. */ +fun Project.validateProjectMavenGroup(groupId: String) { + if (groupId.contains('-')) { + throw GradleException( + "Invalid Maven group! Found invalid character '-' in Maven group \"$groupId\" for " + + "$displayName.\n\nWas this supposed to be a sub-artifact of an existing group, " + + "ex. \"x.y:y-z\" rather than \"x.y-z:z\"?" + ) + } +} + +// Translate common phrases and marketing names into Maven name component equivalents. +private val mavenNameMap = + mapOf( + "android for cars" to "car", + "android wear" to "wear", + "compose glimmer" to "glimmer", + "internationalization" to "i18n", + "kotlin extensions" to "ktx", + "lint checks" to "lint", + "material components" to "material", + "material3 components" to "material3", + "workmanager" to "work", + "windowmanager" to "window", + ) + +// Allow a small set of common Maven name components that don't need to appear in the project name. +private val mavenNameAllowlist = setOf("extension", "extensions", "for", "integration", "with") + +/** Validates the project's Maven name against Jetpack guidelines. */ +fun Project.validateProjectMavenName(mavenName: String, groupId: String) { + // Tokenize the Maven name into components. This is *very* permissive regarding separators, and + // we may want to revisit that policy in the future. + val nameComponents = + mavenName + .lowercase() + .let { name -> + mavenNameMap.entries.fold(name) { newName, entry -> + newName.replace(entry.key, entry.value) + } + } + .split(" ", ",", ":", "-") + .toMutableList() - mavenNameAllowlist + + // Remaining components *must* appear in the Maven coordinate. Shortening long (>10 char) words + // to five letters or more is allowed, as is changing the pluralization of words. + nameComponents + .find { nameComponent -> + !name.contains(nameComponent) && + !groupId.contains(nameComponent) && + !(nameComponent.length > 10 && name.contains(nameComponent.substring(0, 5))) && + !(nameComponent.endsWith("s") && name.contains(nameComponent.dropLast(1))) + } + ?.let { missingComponent -> + throw GradleException( + "Invalid Maven name! Found \"$missingComponent\" in Maven name for $displayName, " + + "but not project name.\n\nConsider removing \"$missingComponent\" from" + + "\"$mavenName\"." + ) + } +} + +private const val GROUP_PREFIX = "androidx." + +/** Validates the project structure against Jetpack guidelines. */ +fun Project.validateProjectStructure(groupId: String) { + if (!project.isValidateProjectStructureEnabled()) { + return + } + + val shortGroupId = + if (groupId.startsWith(GROUP_PREFIX)) { + groupId.substring(GROUP_PREFIX.length) + } else { + groupId + } + + // Fully-qualified Gradle project name should match the Maven coordinate. + val expectName = ":${shortGroupId.replace(".",":")}:${project.name}" + val actualName = project.path + if (expectName != actualName) { + throw GradleException( + "Invalid project structure! Expected $expectName as project name, found $actualName" + ) + } + + // Project directory should match the Maven coordinate. + val expectDir = shortGroupId.replace(".", File.separator) + "${File.separator}${project.name}" + // Canonical projectDir is needed because sometimes, at least in tests, on OSX, supportRoot + // starts with /var, and projectDir starts with /private/var (which are the same thing) + val canonicalProjectDir = project.projectDir.canonicalFile + val actualDir = + canonicalProjectDir.toRelativeString(project.getSupportRootFolder().canonicalFile) + if (expectDir != actualDir) { + throw GradleException( + "Invalid project structure! Expected $expectDir as project directory, found " + + actualDir + ) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectCreatorTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectCreatorTask.kt new file mode 100644 index 0000000000000..a90cb71df01fc --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectCreatorTask.kt @@ -0,0 +1,675 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This suppression is for usage of UserInputHandler and UserQuestions (from Gradle). +// For more information, see https://github.com/gradle/gradle/issues/28216 +@file:Suppress("InternalGradleApiUsage") + +package androidx.build + +import com.google.common.annotations.VisibleForTesting +import java.io.File +import java.time.LocalDate +import org.gradle.api.DefaultTask +import org.gradle.api.internal.tasks.userinput.UserInputHandler +import org.gradle.api.internal.tasks.userinput.UserQuestions +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault +import org.tomlj.Toml +import org.tomlj.TomlParseResult +import org.tomlj.TomlTable + +@DisableCachingByDefault(because = "Interactive task, must run every time") +abstract class ProjectCreatorTask : DefaultTask() { + private val supportDir = project.getSupportRootFolder() + + @TaskAction + fun exec() { + val spec: ProjectSpec = promptForProjectSpec() + val catalogEditor = VersionCatalogEditor(File(supportDir, "libraryversions.toml"), spec) + val settingsEditor = GradleSettingsEditor(File(supportDir, "settings.gradle")) + val docsTotBuildGradleEditor = + DocsTotBuildGradleEditor(File(supportDir, "docs-tip-of-tree/build.gradle")) + val projectGenerator = ProjectGenerator() + + catalogEditor.updateLibraryVersionsToml() + settingsEditor.updateSettingsGradle(spec) + docsTotBuildGradleEditor.updateDocsTotBuildGradle(spec) + projectGenerator.createDirectories(spec, catalogEditor.isGroupIdAtomic()) + + printTodoList(spec) + } + + private fun promptForProjectSpec(): ProjectSpec { + // This println here is intentional, it allows any error messages from groupIdIsValid() and + // artifactIdIsValid() to be shown right after the respective prompt. For some reason, + // without this empty println, those printlns in the validation functions don't get flushed + // until after the user tries the prompt for the second time. + println() + val userInput = services.get(UserInputHandler::class.java) + + var groupId = "" + do { + groupId = + userInput + .askUser { interaction: UserQuestions -> + interaction.askQuestion( + "Enter group id (must start with 'androidx', e.g. androidx.core)", + "none", + ) + } + .get() + } while (!isGroupIdValid(groupId)) + var artifactId = "" + do { + artifactId = + userInput + .askUser { interaction: UserQuestions -> + interaction.askQuestion("Enter artifact id (e.g. core-telecom)", "none") + } + .get() + } while (!isArtifactIdValid(groupId, artifactId)) + + val projectTypeName = + userInput + .askUser { interaction: UserQuestions -> + interaction.selectOption( + "Please choose the type of project you would like to create", + ProjectType.entries.map { it.description }, + ProjectType.ANDROID_LIBRARY.description, + ) + } + .get() + val projectDescription = + userInput + .askUser { interaction: UserQuestions -> + interaction.askQuestion("Enter project description", "none") + } + .get() + + val projectType = ProjectType.entries.find { it.description == projectTypeName } + + if (projectType == null) { + error("Unknown project type: $projectTypeName") + } + + return ProjectSpec(groupId, artifactId, projectType, projectDescription, supportDir) + } + + private fun printTodoList(projectSpec: ProjectSpec) { + val buildGradlePath = projectSpec.fullArtifactPath.resolve("build.gradle") + val ownersFilePath = projectSpec.fullArtifactPath.resolve("OWNERS") + val packageDocsPath = + getPackageDocumentationFileDir(projectSpec) + .resolve( + getPackageDocumentationFilename( + projectSpec.groupIdWithPrefix, + projectSpec.artifactId, + ) + ) + + println( + """ + --- + Created the project. The following TODOs need to be completed by you: + + 1. Check that the OWNERS file is in the correct place. It is currently at: + ${ownersFilePath.path} + 2. Add your name (and others) to the OWNERS file: + ${ownersFilePath.path} + 3. Check that the correct library version is assigned in the build.gradle: + ${buildGradlePath.path} + 4. Fill out the project/module name in the build.gradle: + ${buildGradlePath.path} + 5. Update the project/module package documentation: + ${packageDocsPath.path} + 6. Check the libraryversions.toml file: + ${supportDir.resolve("libraryversions.toml").path} + """ + .trimIndent() + ) + } +} + +@VisibleForTesting +internal class GradleSettingsEditor(val settingsGradleFile: File) { + fun updateSettingsGradle(spec: ProjectSpec) { + val settingsLines = settingsGradleFile.readLines().toMutableList() + val newLine = getNewSettingsGradleLine(spec) + + val insertLine = + settingsLines.indexOfFirst { it.contains("includeProject") && it > newLine } + if (insertLine != -1) { + settingsLines.add(insertLine, newLine) + } else { + settingsLines.add(newLine) + } + + settingsGradleFile.writeText(settingsLines.joinToString("\n") + "\n") + } + + private fun getNewSettingsGradleLine(spec: ProjectSpec): String { + val buildType = getBuildType(spec) + val gradlePath = getGradleProjectCoordinates(spec.groupId, spec.artifactId) + return "includeProject(\"$gradlePath\", [BuildType.$buildType])" + } + + private fun getBuildType(spec: ProjectSpec): String { + return if (isComposeProject(spec.groupId, spec.artifactId)) { + "COMPOSE" + } else if (spec.projectType == ProjectType.KMP) { + "KMP" + } else { + "MAIN" + } + } +} + +@VisibleForTesting +internal class VersionCatalogEditor(val tomlFile: File, val spec: ProjectSpec) { + + /** + * Checks if a group ID is atomic using the libraryversions.toml file. + * + * If one already exists, then this function evaluates the group id and returns the appropriate + * atomicity. Otherwise, it returns False. + * + * Example of an atomic library group: ACTIVITY = { group = "androidx.work", atomicGroupVersion + * = "WORK" } Example of a non-atomic library group: WEAR = { group = "androidx.wear" } + */ + fun isGroupIdAtomic(): Boolean { + val tomlParseResult: TomlParseResult = Toml.parse(tomlFile.toPath()) + val groupsTable = tomlParseResult.getTable("groups") ?: return false + val groupEntry = groupsTable.getTable(getGroupIdVersionMacro(spec.groupId)) + return groupEntry?.contains("atomicGroupVersion") == true + } + + fun updateLibraryVersionsToml() { + val tomlLines = tomlFile.readLines().toMutableList() + val tomlParseResult: TomlParseResult = Toml.parse(tomlFile.toPath()) + + registerVersion(tomlLines, tomlParseResult, spec.groupId) + registerGroup(tomlLines, tomlParseResult, spec.groupIdWithPrefix) + + tomlFile.writeText(tomlLines.joinToString("\n", postfix = "\n")) + } + + private fun registerVersion( + tomlLines: MutableList, + parseResult: TomlParseResult, + groupId: String, + ) { + // Update [versions] section + + val groupIdVersionMacro = getGroupIdVersionMacro(groupId) + + val versionsTable: TomlTable? = parseResult.getTable("versions") + val versionExists = versionsTable?.contains(groupIdVersionMacro) == true + + if (!versionExists) { + val versionsBlockStart = tomlLines.indexOf("[versions]") + val groupsBlockStart = tomlLines.indexOf("[groups]") + + val newVersionLine = "$groupIdVersionMacro = \"1.0.0-alpha01\"" + var versionInsertIndex = groupsBlockStart // Default insert point + + // Find the correct alphabetical insertion index within the [versions] block + for (i in versionsBlockStart + 1 until groupsBlockStart) { + val line = tomlLines[i].trim() + if (line.isEmpty() || line.startsWith("#") || line.startsWith("[")) continue + if (line > newVersionLine) { + versionInsertIndex = i + break + } + } + tomlLines.add(versionInsertIndex, newVersionLine) + println("Added version entry for '$groupIdVersionMacro' in libraryversions.toml.") + } else { + println( + "Version entry for '$groupIdVersionMacro' already exists in libraryversions.toml. Skipping." + ) + } + } + + private fun registerGroup( + tomlLines: MutableList, + parseResult: TomlParseResult, + groupId: String, + ) { + // update [groups] section + + val groupIdVersionMacro = getGroupIdVersionMacro(groupId) + + // Re-find groupsBlockStart as tomlLines might have been modified + val newGroupsBlockStart = tomlLines.indexOf("[groups]") + + val groupsTable: TomlTable? = parseResult.getTable("groups") + // Check if any key within [groups] has a sub-table with 'group = "$groupId"' + val groupExists = + groupsTable?.keySet()?.any { key -> + val groupSpec = groupsTable.getTable(key) + groupSpec?.getString("group") == groupId + } == true + + if (!groupExists) { + val newGroupLine = + """$groupIdVersionMacro = { group = "$groupId", atomicGroupVersion = "versions.$groupIdVersionMacro" }""" + var groupInsertIndex = tomlLines.size // Default insert at the end + + // Find the correct alphabetical insertion index within the [groups] block + for (i in newGroupsBlockStart + 1 until tomlLines.size) { + val line = tomlLines[i].trim() + if (line.isEmpty() || line.startsWith("#") || line.startsWith("[")) continue + if (line > newGroupLine) { + groupInsertIndex = i + break + } + } + tomlLines.add(groupInsertIndex, newGroupLine) + println("Added group entry for '$groupId' in libraryversions.toml.") + } else { + println("Group entry for '$groupId' already exists in libraryversions.toml. Skipping.") + } + } +} + +internal class DocsTotBuildGradleEditor(val docsTotBuildGradleFile: File) { + fun updateDocsTotBuildGradle(spec: ProjectSpec) { + if ( + ("test" in spec.groupId || + "test" in spec.artifactId || + "benchmark" in spec.groupId || + "benchmark" in spec.artifactId) + ) { + println( + "Skipping docs-tip-of-tree update for test/benchmark library " + + "$spec.groupId:$spec.artifactId. Please add manually if needed." + ) + return + } + + val newLine = spec.getNewDocsTotBuildGradleLine() ?: return + val docLines = docsTotBuildGradleFile.readLines().toMutableList() + + val dependenciesBlockStart = + docLines.indexOfFirst { it.trim().startsWith("dependencies {") } + if (dependenciesBlockStart == -1) { + error("Error: Could not find 'dependencies {' block in " + docsTotBuildGradleFile.path) + } + + val newProjectPart = newLine.split("project")[1] + val insertLine = + docLines.indexOfFirst { + it.contains("project") && it.substringAfter("project") >= newProjectPart + } + + if (insertLine != -1) { + docLines.add(insertLine, newLine) + } else { + docLines.add(dependenciesBlockStart + 1, newLine) + } + + docsTotBuildGradleFile.writeText(docLines.joinToString("\n", postfix = "\n")) + } + + private fun ProjectSpec.getNewDocsTotBuildGradleLine(): String? { + if ("sample" in artifactId) { + println( + "Auto-detected sample project. Please add the sample dependency to the " + + "androidx block of the library's build.gradle file." + ) + return null + } + val gradlePath = getGradleProjectCoordinates(groupId, artifactId) + return """ ${if (projectType == ProjectType.KMP) "kmpDocs" else "docs"}(project("$gradlePath"))""" + } +} + +@VisibleForTesting +internal class ProjectGenerator { + fun createDirectories(spec: ProjectSpec, isGroupIdAtomic: Boolean) { + spec.fullArtifactPath.mkdirs() + + // create src dir + createSrcDir(spec) + + // create OWNERS file + val ownersFile = File(spec.fullArtifactPath, "OWNERS") + ownersFile.writeText("# example@google.com\n") + + // create build.gradle file + val buildGradleFile = File(spec.fullArtifactPath, "build.gradle") + buildGradleFile.writeText(spec.getBuildGradleText(isGroupIdAtomic)) + + // Write current.txt, res-current.txt, and restricted_current.txt + for (signatureFileName: String in listOf("current", "res-current", "restricted_current")) { + val txtFile = File(spec.fullArtifactPath, "api/$signatureFileName.txt") + txtFile.parentFile.mkdirs() + txtFile.writeText( + if (signatureFileName != "res-current") "// Signature format: 4.0\n" else "" + ) + } + } + + private fun createSrcDir(spec: ProjectSpec) { + val basePath = if (spec.projectType == ProjectType.KMP) "src/commonMain" else "src/main" + val fullPath = + "$basePath/${spec.projectType.getLanguage()}/androidx/${ + spec.groupId.replace( + ".", + "/", + ) + }" + + val docFile = + File( + spec.fullArtifactPath, + "$fullPath/${getPackageDocumentationFilename(spec.groupId, spec.artifactId)}", + ) + docFile.parentFile.mkdirs() + docFile.writeText(spec.toPackageDocsText()) + + if (spec.projectType == ProjectType.JAVA) { + val packageInfoFile = File(spec.fullArtifactPath, "$fullPath/package-info.java") + + packageInfoFile.writeText(spec.getPackageInfoFileText()) + } + + if (spec.projectType == ProjectType.KMP) { + val testFile = + File( + spec.fullArtifactPath, + "${fullPath.replace("commonMain", "commonTest")}/Test.kt", + ) + testFile.parentFile.mkdirs() + testFile.writeText(spec.createTestFileText()) + } + } + + private fun ProjectSpec.getPackageInfoFileText(): String { + return """ + ${getAOSPHeader()} + + package androidx.${groupId}.${artifactId.removePrefix(groupId.split(".").last()).removePrefix("-").replace("-", ".")} + """ + .trimIndent() + } + + private fun getAOSPHeader(): String { + return """ + /* + * Copyright ${getYear()} The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + """ + } + + private fun ProjectSpec.createTestFileText(): String { + return """ + ${getAOSPHeader()} + package androidx.${groupId} + + class Test { + } + """ + .trimIndent() + } + + private fun ProjectSpec.toPackageDocsText(): String { + return """ + # Module root + + $groupId $artifactId + + # Package ${generatePackageName(groupId, artifactId)} + + Insert package level documentation here + """ + .trimIndent() + } + + private fun ProjectSpec.getBuildGradleText(isGroupIdAtomic: Boolean): String { + return """ + ${getAOSPHeader()} + + /** + * This file was created using the `createProject` gradle task (./gradlew createProject) + * + * Please use the task when creating a new project, rather than copying an existing project and + * modifying its settings. + */ + import androidx.build.SoftwareType + ${if (projectType == ProjectType.KMP) "import androidx.build.PlatformIdentifier" else ""} + + plugins { + id("AndroidXPlugin") + ${getGradlePlugin()} + } + + dependencies { + // Add dependencies here + } + + ${ + when (projectType) { + ProjectType.KMP -> """ + androidXMultiplatform { + ${getMultiplatformBuildGradleText()} + } + """ + ProjectType.ANDROID_LIBRARY -> """ + android { + namespace = "${generatePackageName(groupId, artifactId)}" + } + """ + ProjectType.JAVA -> """ + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + """ + } + } + + androidx { + name = "${groupId}:${artifactId}" + type = SoftwareType.${getLibraryType(artifactId)} + ${if (isGroupIdAtomic) "" else "mavenVersion = LibraryVersions.${getGroupIdVersionMacro(groupId)}"} + inceptionYear = "${getYear()}" + description = "$description" + } + """ + .trimIndent() + } + + private fun ProjectSpec.getMultiplatformBuildGradleText(): String { + return """ + ${ + if (isComposeProject(groupId, artifactId)) { + """ + androidLibrary { + namespace = "androidx.compose.${artifactId.removePrefix("compose-").replace("-", ".")}" + compileSdk { version = release(35) } + } + jvmStubs() + linuxX64Stubs() + + defaultPlatform(PlatformIdentifier.ANDROID) + """ + } else { + """ + ios() + js() + jvm() + linux() + mac() + mingwX64() + tvos() + wasmJs() + watchos() + + defaultPlatform(PlatformIdentifier.JVM) + """ + } + } + + sourceSets { + commonMain.dependencies { + } + + commonTest.dependencies { + } + ${if (isComposeProject(groupId, artifactId)) { + """ + + commonStubsMain.dependsOn(commonMain) + jvmStubsMain.dependsOn(commonStubsMain) + linuxx64StubsMain.dependsOn(commonStubsMain) + """ + } else { "" }} + } + """ + } + + private fun ProjectSpec.getGradlePlugin(): String { + if (isComposeProject(groupId, artifactId)) { + return """id("AndroidXComposePlugin")""" + } + return when (this.projectType) { + ProjectType.ANDROID_LIBRARY -> """id("com.android.library")""" + ProjectType.KMP -> "" + ProjectType.JAVA -> """id("java-library")""" + } + } + + private fun ProjectType.getLanguage(): String { + return when (this) { + ProjectType.ANDROID_LIBRARY -> "kotlin" + ProjectType.KMP -> "kotlin" + ProjectType.JAVA -> "java" + } + } + + private fun getYear(): String = LocalDate.now().year.toString() +} + +private fun getPackageDocumentationFileDir(spec: ProjectSpec): File { + val subPath = + when (spec.projectType) { + ProjectType.ANDROID_LIBRARY -> { + "src/main/kotlin/" + } + ProjectType.KMP -> { + "src/commonMain/kotlin/" + } + ProjectType.JAVA -> { + "src/main/java/" + } + } + spec.groupIdWithPrefix.replace('.', '/') + return File(spec.fullArtifactPath, subPath) +} + +@VisibleForTesting +internal enum class ProjectType(val description: String) { + ANDROID_LIBRARY("Android (AAR)"), + KMP("KMP (All platforms) (AAR)"), + JAVA("Java (JVM - JAR)"), +} + +@VisibleForTesting +internal data class ProjectSpec( + val groupIdWithPrefix: String, + val artifactId: String, + val projectType: ProjectType, + val description: String, + val supportRoot: File, +) { + val groupId = groupIdWithPrefix.removePrefix("androidx.") + + val fullArtifactPath = + File(supportRoot, groupId.replace('.', '/')).resolve(artifactId.removePrefix("compose-")) +} + +@VisibleForTesting +internal fun isGroupIdValid(groupId: String): Boolean { + if (!groupId.startsWith("androidx.")) { + println("Group ID must start with 'androidx'.") + return false + } else if ( + listOf("compose", "wear", "xr").any { it == groupId.split(".")[1] } && + groupId.split(".").size == 2 + ) { + println( + "New ${groupId.split(".")[1]} projects must be nested inside an existing sub-project" + ) + return false + } else { + return true + } +} + +@VisibleForTesting +internal fun isArtifactIdValid(groupId: String, artifactId: String): Boolean { + val finalGroupWord = groupId.substringAfterLast('.') + if (!artifactId.startsWith(finalGroupWord)) { + println("Artifact ID must start with the last segment of the Group ID ($finalGroupWord).") + return false + } + return true +} + +private fun isComposeProject(groupId: String, artifactId: String): Boolean = + "compose" in groupId || "compose" in artifactId + +internal fun generatePackageName(groupId: String, artifactId: String): String { + val groupLast = groupId.split('.').last() + + val suffix = artifactId.removePrefix(groupLast).replace('-', '.').trim('.') + + return if (suffix.isEmpty()) groupId else "$groupId.$suffix" +} + +internal fun getGroupIdVersionMacro(groupId: String): String { + return groupId.removePrefix("androidx.").replace(".", "_").uppercase() +} + +internal fun getGradleProjectCoordinates(groupId: String, artifactId: String): String { + return ":${groupId.removePrefix("androidx.").replace(".", ":")}:${artifactId.removePrefix("compose-")}" +} + +internal fun getLibraryType(artifactId: String): String = + when { + "sample" in artifactId -> "SAMPLES" + "compiler" in artifactId -> "ANNOTATION_PROCESSOR" + "lint" in artifactId -> "LINT" + "inspection" in artifactId -> "IDE_PLUGIN" + else -> "PUBLISHED_LIBRARY" + } + +internal fun getPackageDocumentationFilename(groupId: String, artifactId: String): String { + return "androidx-${groupId.replace('.', '-')}-$artifactId-documentation.md" +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectExt.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectExt.kt new file mode 100644 index 0000000000000..d03e7746fbc3e --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectExt.kt @@ -0,0 +1,67 @@ +/** + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package androidx.build + +import java.io.File +import java.util.Collections +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider + +/** Holder class used for lazily registering tasks using the new Lazy task execution API. */ +data class LazyTaskRegistry( + private val names: MutableSet = Collections.synchronizedSet(mutableSetOf()) +) { + fun once(name: String, f: () -> T): T? { + if (names.add(name)) { + return f() + } + return null + } + + companion object { + private const val KEY = "AndroidXAutoRegisteredTasks" + private val lock = ReentrantLock() + + fun get(project: Project): LazyTaskRegistry { + val existing = project.extensions.findByName(KEY) as? LazyTaskRegistry + if (existing != null) { + return existing + } + return lock.withLock { + project.extensions.findByName(KEY) as? LazyTaskRegistry + ?: LazyTaskRegistry().also { project.extensions.add(KEY, it) } + } + } + } +} + +inline fun Project.maybeRegister( + name: String, + crossinline onConfigure: (T) -> Unit, + crossinline onRegister: (TaskProvider) -> Unit, +): TaskProvider { + @Suppress("UNCHECKED_CAST") + return LazyTaskRegistry.get(project).once(name) { + tasks.register(name, T::class.java) { onConfigure(it) }.also(onRegister) + } ?: tasks.named(name) as TaskProvider +} + +internal fun Project.lazyReadFile(fileName: String): Provider { + val fileProperty = objects.fileProperty().fileValue(File(getSupportRootFolder(), fileName)) + return providers.fileContents(fileProperty).asText +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt new file mode 100644 index 0000000000000..3f7983b195d2b --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectParser.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.io.File +import java.util.concurrent.ConcurrentHashMap +import org.gradle.api.Project +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +abstract class ProjectParser : BuildService { + @Transient val cache: MutableMap = ConcurrentHashMap() + + fun get(buildFile: File): ParsedProject { + return cache.getOrPut(key = buildFile) { + val text = buildFile.readLines() + parseProject(text) + } + } + + private fun parseProject(fileLines: List): ParsedProject { + var softwareType: String? = null + var publish: String? = null + var specifiesVersion = false + fileLines.forEach { line -> + if (softwareType == null) + softwareType = line.extractVariableValue(" type = SoftwareType.") + if (publish == null) publish = line.extractVariableValue(" publish = Publish.") + if (line.contains("mavenVersion =")) specifiesVersion = true + } + val softwareTypeEnum = softwareType?.let { SoftwareType.valueOf(it) } ?: SoftwareType.UNSET + return ParsedProject(softwareType = softwareTypeEnum, specifiesVersion = specifiesVersion) + } + + data class ParsedProject(val softwareType: SoftwareType, val specifiesVersion: Boolean) { + fun shouldPublish(): Boolean = softwareType.publish.shouldPublish() + + fun shouldRelease(): Boolean = softwareType.publish.shouldRelease() + } +} + +private fun String.extractVariableValue(prefix: String): String? { + val declarationIndex = this.indexOf(prefix) + if (declarationIndex >= 0) { + val suffix = this.substring(declarationIndex + prefix.length) + val spaceIndex = suffix.indexOf(" ") + if (spaceIndex > 0) return suffix.substring(0, spaceIndex) + return suffix + } + return null +} + +fun Project.parse(): ProjectParser.ParsedProject { + return parseBuildFile(project.buildFile) +} + +fun Project.parseBuildFile(buildFile: File): ProjectParser.ParsedProject { + val parserProvider = + project.gradle.sharedServices.registerIfAbsent( + "ProjectParser", + ProjectParser::class.java, + ) {} + val parser = parserProvider.get() + return parser.get(buildFile) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.kt new file mode 100644 index 0000000000000..8fddba25d3581 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Project +import org.gradle.api.UnknownProjectException + +// Resolves the given project, and if it is not found, +// throws an exception that mentions the active project subset, if any (MAIN, COMPOSE, ...) +fun Project.resolveProject(projectSpecification: String): Project { + try { + return project.project(projectSpecification) + } catch (e: UnknownProjectException) { + val subset = project.getProjectSubset() + val subsetDescription = + if (subset == null) { + "" + } else { + " in subset $subset" + } + throw UnknownProjectException( + "Project $projectSpecification not found$subsetDescription", + e, + ) + } +} + +/** + * Returns the name of the subset of projects participating in the build. + * + * Project subsets are defined in settings.gradle and allow including only a subset of projects in + * the build, to make project configuration run more quickly. + */ +fun Project.getProjectSubset(): String? { + val envProp = project.providers.environmentVariable("ANDROIDX_PROJECTS") + if (envProp.isPresent) { + return envProp.get().uppercase() + } + return null +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/PublishingHelper.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/PublishingHelper.kt new file mode 100644 index 0000000000000..8a37514941de5 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/PublishingHelper.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.component.AdhocComponentWithVariants + +internal fun Project.registerAsComponentForPublishing(gradleVariant: Configuration) = + components.configureEach { + // Android Library project 'release' component + // Java Library project 'java' component + if (it.name == "release" || it.name == "java") { + it as AdhocComponentWithVariants + it.addVariantsFromConfiguration(gradleVariant) {} + } + } + +internal fun Project.registerAsComponentForKmpPublishing(gradleVariant: Configuration) = + components.configureEach { + // Multiplatform library 'adhocKotlin' component + // https://github.com/JetBrains/kotlin/blob/bf6cb00fa8db7879c323bad863f58a0545c3d655/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformPublishing.kt#L20 + if (it.name == "adhocKotlin") { + it as AdhocComponentWithVariants + it.addVariantsFromConfiguration(gradleVariant) {} + } + } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/Release.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/Release.kt new file mode 100644 index 0000000000000..326c60e605817 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/Release.kt @@ -0,0 +1,227 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.build + +import java.io.FileOutputStream +import java.util.Calendar +import java.util.GregorianCalendar +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.work.DisableCachingByDefault + +/** Zips all artifacts to publish. */ +@DisableCachingByDefault(because = "Zip tasks are not worth caching according to Gradle") +abstract class GMavenZipTask : DefaultTask() { + + /** Whether this build adds automatic constraints between projects in the same group */ + @Internal val shouldAddGroupConstraints = project.shouldAddGroupConstraints() + + /** Repository containing artifacts to include */ + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val projectRepositoryDir: DirectoryProperty + + /** Zip file to save artifacts to */ + @get:OutputFile abstract val archiveFile: RegularFileProperty + + @TaskAction + fun createZip() { + if (!shouldAddGroupConstraints.get() && !isSnapshotBuild()) { + throw GradleException( + """ + Cannot publish artifacts without setting -P$ADD_GROUP_CONSTRAINTS=true + + This property is required when building artifacts to publish + + (but this property can reduce remote cache usage so it is disabled by default) + + See AndroidXGradleProperties.kt for more information about this property + """ + .trimIndent() + ) + } + val sourceDir = projectRepositoryDir.get().asFile + ZipOutputStream(FileOutputStream(archiveFile.get().asFile)).use { zipOut -> + zipOut.putNextEntry( + // Top-level of the ZIP to align with Maven's expected repository structure + ZipEntry("m2repository/").also { it.time = CONSTANT_TIME_FOR_ZIP_ENTRIES } + ) + zipOut.closeEntry() + + sourceDir.walkTopDown().forEach { fileOrDir -> + if (fileOrDir == sourceDir) return@forEach + + val relativePath = fileOrDir.relativeTo(sourceDir).invariantSeparatorsPath + val entryName = + "m2repository/$relativePath" + if (fileOrDir.isDirectory) "/" else "" + + zipOut.putNextEntry( + ZipEntry(entryName).also { it.time = CONSTANT_TIME_FOR_ZIP_ENTRIES } + ) + if (fileOrDir.isFile) { + fileOrDir.inputStream().use { it.copyTo(zipOut) } + } + zipOut.closeEntry() + } + } + } +} + +/** Handles creating various release tasks that create zips for the maven upload and local use. */ +object Release { + @Suppress("MemberVisibilityCanBePrivate") + const val PROJECT_ARCHIVE_ZIP_TASK_NAME = "createProjectZip" + private const val FULL_ARCHIVE_TASK_NAME = "createArchive" + private const val ALL_ARCHIVES_TASK_NAME = "createAllArchives" + const val DEFAULT_PUBLISH_CONFIG = "release" + const val PROJECT_ZIPS_FOLDER = "per-project-zips" + private const val GLOBAL_ZIP_PREFIX = "top-of-tree-m2repository" + + /** + * Registers the project to be included in its group's zip file as well as the global zip files. + */ + fun register(project: Project, androidXExtension: AndroidXExtension) { + if (!androidXExtension.shouldPublish.get()) { + project.logger.info( + "project ${project.name} isn't part of release," + + " because its \"publish\" property is explicitly set to Publish.NONE" + ) + return + } + if (!androidXExtension.shouldRelease.get() && !isSnapshotBuild()) { + project.logger.info( + "project ${project.name} isn't part of release, because its" + + " \"publish\" property is SNAPSHOT_ONLY, but it is not a snapshot build" + ) + return + } + if (!androidXExtension.versionIsSet) { + throw IllegalArgumentException( + "Cannot register a project to release if it does not have a mavenVersion set up" + ) + } + + val projectZipTask = + getProjectZipTask(project, androidXExtension.isIsolatedProjectsEnabled()) + val zipTasks = + listOfNotNull( + projectZipTask, + getGlobalFullZipTask(project, androidXExtension.isIsolatedProjectsEnabled()), + ) + + val publishTask = project.tasks.named("publish") + zipTasks.forEach { it.configure { zipTask -> zipTask.dependsOn(publishTask) } } + } + + /** Registers an archive task as a dependency of the anchor task */ + private fun Project.addToAnchorTask(task: TaskProvider) { + val archiveAnchorTask: TaskProvider = + project.rootProject.maybeRegister( + name = ALL_ARCHIVES_TASK_NAME, + onConfigure = { archiveTask: VerifyLicenseAndVersionFilesTask -> + archiveTask.group = "Distribution" + archiveTask.description = "Builds all archives for publishing" + archiveTask.repositoryDirectory.set( + project.rootProject.getRepositoryDirectory() + ) + }, + onRegister = {}, + ) + archiveAnchorTask.configure { it.dependsOn(task) } + } + + /** + * Creates and returns the task that includes all projects regardless of their release status. + */ + private fun getGlobalFullZipTask( + project: Project, + projectIsolationEnabled: Boolean, + ): TaskProvider? { + if (projectIsolationEnabled) return null + return project.rootProject.maybeRegister( + name = FULL_ARCHIVE_TASK_NAME, + onConfigure = { task: GMavenZipTask -> + task.archiveFile.set( + project.getDistributionDirectory().file("${getZipName(GLOBAL_ZIP_PREFIX)}.zip") + ) + task.projectRepositoryDir.set(project.getRepositoryDirectory()) + }, + onRegister = { taskProvider: TaskProvider -> + project.addToAnchorTask(taskProvider) + }, + ) + } + + private fun getProjectZipTask( + project: Project, + projectIsolationEnabled: Boolean, + ): TaskProvider { + val taskProvider = + project.tasks.register(PROJECT_ARCHIVE_ZIP_TASK_NAME, GMavenZipTask::class.java) { + it.archiveFile.set( + project.getDistributionDirectory().file(project.getProjectZipPath()) + ) + it.projectRepositoryDir.set(project.getPerProjectRepositoryDirectory()) + } + if (!projectIsolationEnabled) { + project.addToAnchorTask(taskProvider) + project.addZipToAttestation( + taskProvider.map { task -> + task.archiveFile + .get() + .asFile + .toRelativeString(project.getDistributionDirectory().get().asFile) + } + ) + } + return taskProvider + } +} + +private fun Project.projectZipPrefix(): String { + return "${project.group}-${project.name}" +} + +private fun getZipName(fileNamePrefix: String) = "$fileNamePrefix-all" + +fun Project.getProjectZipPath(): String { + return Release.PROJECT_ZIPS_FOLDER + + "/" + + // We pass in a "" because that mimics not passing the group to getParams() inside + // the getProjectZipTask function + getZipName(projectZipPrefix()) + + "-${project.version}.zip" +} + +/** + * Strip timestamps from the zip entries to generate consistent output. Set to be ths same as what + * Gradle uses: + * https://github.com/gradle/gradle/blob/master/platforms/core-runtime/files/src/main/java/org/gradle/api/internal/file/archive/ZipEntryConstants.java + */ +private val CONSTANT_TIME_FOR_ZIP_ENTRIES = + GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0).timeInMillis diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/Samples.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/Samples.kt new file mode 100644 index 0000000000000..0e3e1dca9cfe5 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/Samples.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.DocsType +import org.gradle.api.attributes.LibraryElements +import org.gradle.api.attributes.Usage +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.named +import org.gradle.work.DisableCachingByDefault + +/** + * Used to configure a project that will be providing documentation samples. + * + * Can only be called once so only one samples library can exist per library b/318840087. + */ +internal fun Project.configureSamplesProject() { + fun Configuration.setResolveSources() { + // While a sample library can have more dependencies than the library it has samples + // for, in Studio sample code is not executable or inspectable, so we don't need them. + isTransitive = false + isCanBeConsumed = false + attributes { + it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage.JAVA_RUNTIME)) + it.attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category.DOCUMENTATION), + ) + it.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + project.objects.named(DocsType.SOURCES), + ) + it.attribute( + LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, + project.objects.named(LibraryElements.JAR), + ) + } + } + + val samplesConfiguration = + project.configurations.register("samples") { + it.isCanBeConsumed = false + it.isCanBeResolved = true + it.setResolveSources() + } + + project.tasks.register("copySampleSourceJars", LazyInputsCopyTask::class.java) { task -> + task.inputJars.from(samplesConfiguration.map { it.incoming.files }) + val srcJarFilename = "${project.name}-${project.version}-samples-sources.jar" + task.destinationJar.set(project.layout.buildDirectory.file(srcJarFilename)) + } +} + +/** + * This is necessary because we need to delay artifact resolution until after configuration. If one + * sample is used by multiple libraries (e.g. paging-samples) it is copied several times. This is to + * avoid caching failures. There should be a better way that avoids needing this. + */ +@DisableCachingByDefault(because = "caching large output files is more expensive than copying") +abstract class LazyInputsCopyTask : DefaultTask() { + @get:[InputFiles PathSensitive(value = PathSensitivity.RELATIVE)] + abstract val inputJars: ConfigurableFileCollection + @get:OutputFile abstract val destinationJar: RegularFileProperty + + @TaskAction + fun copyAction() { + inputJars.files.single().copyTo(destinationJar.get().asFile, overwrite = true) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/SettingsParser.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/SettingsParser.kt new file mode 100644 index 0000000000000..6d038613fb500 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/SettingsParser.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.io.File + +// NOTE: This class is symlinked to +// playground-common/playground-plugin/src/main/kotlin/androidx/build +// Please test playground when modifying it. +/** + * Helper class to parse the settings.gradle file from the main build and extract a list of + * projects. + * + * This is used by Playground projects too, so if it is changed please run `cd room3 && ./gradlew + * tasks` + */ +object SettingsParser { + /** + * Match lines that start with includeProject, followed by a require argument for project gradle + * path and an optional argument for project file path. + */ + private val includeProjectPattern = + Regex( + """^[\n\r\s]*includeProject\("(?[a-z0-9-:]*)"(,[\n\r\s]*"(?[a-z0-9-/]+))?.*\).*$""", + setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE), + ) + .toPattern() + + fun findProjects(settingsFile: File): List { + return findProjects(fileContents = settingsFile.readText(Charsets.UTF_8)) + } + + fun findProjects(fileContents: String): List { + val matcher = includeProjectPattern.matcher(fileContents) + val includedProjects = mutableListOf() + while (matcher.find()) { + if (matcher.group().contains("new File")) { + // we don't support explicit project paths in playground + continue + } + // check if is an include project line, if so, extract project gradle path and + // file system path and call the filter + val projectGradlePath = + matcher.group("name") ?: error("Project gradle path should not be null") + val projectFilePath = + matcher.group("path") ?: createFilePathFromGradlePath(projectGradlePath) + includedProjects.add(IncludedProject(projectGradlePath, projectFilePath)) + } + return includedProjects + } + + /** Converts a gradle path (e.g. :a:b:c) to a file path (a/b/c) */ + private fun createFilePathFromGradlePath(gradlePath: String): String { + return gradlePath.trimStart(':').replace(':', '/') + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/StringUtils.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/StringUtils.kt new file mode 100644 index 0000000000000..622a127afd41b --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/StringUtils.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.util.Locale + +internal fun String.capitalize() = replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/UnpackedStubAarTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/UnpackedStubAarTask.kt new file mode 100644 index 0000000000000..01a82e5cec565 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/UnpackedStubAarTask.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault +import org.intellij.lang.annotations.Language + +/** + * Creates a directory representing a stub (essentially empty) .aar This directory can be zipped to + * make an actual .aar + */ +@DisableCachingByDefault(because = "Doesn't benefit from caching") +abstract class UnpackedStubAarTask : DefaultTask() { + @get:Input abstract val aarPackage: Property + @get:Input abstract val minSdkVersion: Property + @get:OutputDirectory abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + // setup + val outputDir = outputDir.asFile.get() + outputDir.deleteRecursively() + outputDir.mkdirs() + // write AndroidManifest.xml + val manifestFile = File("$outputDir/AndroidManifest.xml") + val aarPackage = aarPackage.get() + @Language("xml") + val manifestText = + """ + + + + """ + .trimIndent() + manifestFile.writeText(manifestText) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/UnzipChromeBuildService.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/UnzipChromeBuildService.kt new file mode 100644 index 0000000000000..eeec110259744 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/UnzipChromeBuildService.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.io.File +import java.util.Locale +import javax.inject.Inject +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +/** A build service that unzips Chrome prebuilts for use in other Gradle tasks. */ +abstract class UnzipChromeBuildService +@Inject +constructor( + private val archiveOperations: ArchiveOperations, + private val fileSystemOperations: FileSystemOperations, +) : BuildService { + + interface Parameters : BuildServiceParameters { + /** Location of Chrome prebuilts. */ + val browserDir: DirectoryProperty + + /** Location to unzip to. */ + val unzipToDir: DirectoryProperty + } + + val chromePath: String by lazy { unzipChrome() } + + /** Unzips the Chrome prebuilt for the current OS and returns the path of the executable. */ + private fun unzipChrome(): String { + val osName = chromeBinOsSuffix() + val chromeZip = + File(parameters.browserDir.get().asFile, "chrome-headless-shell-$osName.zip") + + fileSystemOperations.copy { + it.from(archiveOperations.zipTree(chromeZip)) + it.into(parameters.unzipToDir) + } + return parameters.unzipToDir + .get() + .asFile + .resolve("chrome-headless-shell-$osName/chrome-headless-shell") + .path + } +} + +private fun chromeBinOsSuffix() = + when { + System.getProperty("os.name").lowercase(Locale.ROOT).contains("linux") -> "linux64" + System.getProperty("os.arch") == "aarch64" -> "mac-arm64" + else -> "mac-x64" + } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ValidateKotlinModuleFiles.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ValidateKotlinModuleFiles.kt new file mode 100644 index 0000000000000..fa799f005e743 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/ValidateKotlinModuleFiles.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import com.android.SdkConstants.DOT_KOTLIN_MODULE +import com.android.utils.appendCapitalized +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.jetbrains.kotlin.gradle.plugin.KotlinBaseApiPlugin +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper + +internal fun Project.validateKotlinModuleFiles(variantName: String, aar: Provider) { + if ( + (!project.plugins.hasPlugin(KotlinBasePluginWrapper::class.java) || + !project.plugins.hasPlugin(KotlinBaseApiPlugin::class.java)) && + !project.plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java) + ) { + return + } + val validateKotlinModuleFiles = + tasks.register( + "validateKotlinModuleFilesFor".appendCapitalized(variantName), + ValidateModuleFilesTask::class.java, + ) { + it.aar.set(aar) + it.cacheEvenIfNoOutputs() + } + project.addToBuildOnServer(validateKotlinModuleFiles) +} + +@CacheableTask +abstract class ValidateModuleFilesTask() : DefaultTask() { + + @get:Inject abstract val archiveOperations: ArchiveOperations + + @get:PathSensitive(PathSensitivity.NONE) @get:InputFile abstract val aar: RegularFileProperty + + @get:Internal + val fileName: String + get() = aar.get().asFile.name + + @TaskAction + fun execute() { + val fileTree = archiveOperations.zipTree(aar) + val classesJar = + fileTree.find { it.name == "classes.jar" } + ?: throw GradleException("Could not classes.jar in $fileName") + val jarContents = archiveOperations.zipTree(classesJar) + if (jarContents.files.size <= 1) { + // only version file, stub project with no sources. + return + } + jarContents.find { it.name.endsWith(DOT_KOTLIN_MODULE) } + ?: throw GradleException("Could not find .kotlin_module file in $fileName") + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt new file mode 100644 index 0000000000000..c42c7bcf16365 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyDependencyVersionsTask.kt @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.Dependency +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.setProperty + +/** + * Task for verifying the androidx dependency-stability-suffix rule (A library is only as stable as + * its least stable dependency) + */ +@CacheableTask +abstract class VerifyDependencyVersionsTask : DefaultTask() { + + init { + group = "Verification" + description = "Task for verifying the androidx dependency-stability-suffix rule" + } + + @get:Input abstract val version: Property + + @get:Input + val androidXDependencySet: SetProperty = project.objects.setProperty() + + /** + * Iterate through the dependencies of the project and ensure none of them are of an inferior + * release. This means that a beta project should not have any alpha dependencies, an rc project + * should not have any alpha or beta dependencies and a stable version should only depend on + * other stable versions. Dependencies defined with testCompile and friends along with + * androidTestImplementation and similar are excluded from this verification. + */ + @TaskAction + fun verifyDependencyVersions() { + androidXDependencySet.get().forEach { dependency -> verifyDependencyVersion(dependency) } + } + + private fun verifyDependencyVersion(dependency: AndroidXDependency) { + val projectVersion = version.get() + val dependencyVersion = dependency.version + val projectReleasePhase = releasePhase(projectVersion) + if (projectReleasePhase < 0) { + throw GradleException("Project has unexpected release phase $projectVersion") + } + val dependencyReleasePhase = releasePhase(dependencyVersion) + if (dependencyReleasePhase < 0) { + throw GradleException( + "Dependency ${dependency.group}:${dependency.name}" + + ":${dependency.version} has unexpected release phase $dependencyVersion" + ) + } + if (dependencyReleasePhase < projectReleasePhase) { + throw GradleException( + "Project with version ${version.get()} may " + + "not take a dependency on less-stable artifact ${dependency.group}:" + + "${dependency.name}:${dependency.version} for configuration " + + "${dependency.configurationName}. Dependency versions must be at least as " + + "stable as the project version." + ) + } + } + + private fun releasePhase(versionString: String): Int { + // If the version is unspecified then treat as an alpha version. If the depending project's + // version is unspecified then it won't matter, and if the dependency's version is + // unspecified then any non alpha project won't be able to depend on it to ensure safety. + val version = + if (versionString != AndroidXExtension.DEFAULT_UNSPECIFIED_VERSION) { + Version(versionString) + } else { + return 1 + } + return when { + version.isStable() -> 4 + version.isRC() -> 3 + version.isBeta() -> 2 + version.isAlpha() || version.isDev() || version.isPrereleasePrefix("qpreview") -> 1 + else -> -1 + } + } +} + +data class AndroidXDependency( + val group: String, + val name: String, + val version: String, + val configurationName: String, +) : java.io.Serializable { + companion object { + private const val serialVersionUID = 344435634564L + } +} + +internal fun Project.createVerifyDependencyVersionsTask(): + TaskProvider { + val usingMaxDepsVersions = project.usingMaxDepVersions() + val taskProvider = + tasks.register("verifyDependencyVersions", VerifyDependencyVersionsTask::class.java) { task + -> + task.version.set(project.version.toString()) + task.androidXDependencySet.set( + project.provider { + val dependencies = mutableSetOf() + project.configurations.filter(project::shouldVerifyConfiguration).forEach { + configuration -> + configuration.allDependencies.filter(::shouldVerifyDependency).forEach { + dependency -> + dependencies.add( + AndroidXDependency( + dependency.group!!, + dependency.name, + dependency.version!!, + configuration.name, + ) + ) + } + } + dependencies + } + ) + task.onlyIf { + /** + * Ignore -Pandroidx.useMaxDepVersions when verifying dependency versions because it + * is a hypothetical build which is only intended to check for forward + * compatibility. + */ + !usingMaxDepsVersions.get() + } + task.cacheEvenIfNoOutputs() + } + + addToBuildOnServer(taskProvider) + return taskProvider +} + +internal fun Project.shouldVerifyConfiguration(configuration: Configuration): Boolean { + // Only verify configurations that are exported to POM. In an ideal world, this would be an + // inclusion derived from the mappings used by the Maven Publish Plugin; however, since we + // don't have direct access to those, this should remain an exclusion list. + val name = configuration.name + + // Don't check any Android-specific variants of Java plugin configurations -- releaseApi for + // api, debugImplementation for implementation, etc. -- or test configurations. + if (name.startsWith("androidTest")) return false + if (name.startsWith("androidAndroidTest")) return false + if (name.startsWith("androidCommonTest")) return false + if (name.startsWith("androidDeviceTest")) return false + if (name.startsWith("androidReleaseUnitTest")) return false + if (name.startsWith("androidHostTest")) return false + if (name.startsWith("debug")) return false + if (name.startsWith("androidDebug")) return false + if (name.startsWith("releaseAndroidTest")) return false + if (name.startsWith("releaseAnnotationProcessor")) return false + // releaseApi, and releaseImplementation are for declaring dependencies + // for the release variant. They extend the releaseCompileClasspath and + // releaseRuntimeClasspath (both resolvable configurations) respectively. + if (name.startsWith("releaseApi")) return false + if (name.startsWith("releaseImplementation")) return false + if (name.startsWith("releaseTest")) return false + if (name.startsWith("releaseUnitTest")) return false + + if (name.startsWith("test")) return false + if (name.startsWith("jvmTest")) return false + if (name.startsWith("_agp_internal")) return false + + // Don't check any tooling configurations. + if (name == "annotationProcessor") return false + if (name == "errorprone") return false + if (name.startsWith("lint")) return false + if (name.endsWith("LintChecksClasspath")) return false + if (name == "metalava") return false + if (name.startsWith("kotlinBuild")) return false + if (name.startsWith("kotlinCompiler")) return false + if (name.startsWith("kotlinKaptWorkerDependencies")) return false + if (name.startsWith("kotlinKlib")) return false + if (name.startsWith("kapt")) return false + if (name.startsWith("ksp")) return false + + // Don't check bundled inspector configurations. + if (name == "consumeInspector") return false + if (name == "importInspectorImplementation") return false + + // Don't check any configurations that directly bundle the dependencies with the output + if (name == "bundleInside") return false + if (name == "embedThemesDebug") return false + if (name == "embedThemesRelease") return false + + // Don't check any compile-only configurations + if (name.startsWith("compile")) return false + + // allow tip of tree compose compiler + if (name.startsWith("kotlinPlugin")) return false + + // Don't check Hilt compile-only configurations + if (name.startsWith("hiltCompileOnly")) return false + + // Don't check Desktop configurations since we don't publish them anyway + if (name.startsWith("desktop")) return false + if (name.startsWith("skiko")) return false + + // Doesn't affect the .pom / .module + // https://github.com/JetBrains/kotlin/blob/v1.9.10/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/resolvableMetadataConfiguration.kt#L102 + if (name.endsWith("DependenciesMetadata")) return false + + // Don't check KGP internal configuration used for tooling + if (name == "kotlinInternalAbiValidation") return false + + // don't verify test configurations of KMP projects + if (name.contains("TestCompilation")) return false + if (name.contains("TestCompile")) return false + if (name.contains("commonTest", ignoreCase = true)) return false + if (name.contains("nativeTest", ignoreCase = true)) return false + if (name.contains("TestCInterop", ignoreCase = true)) return false + if ( + multiplatformExtension?.targets?.any { + name.contains("${it.name}Test", ignoreCase = true) + } == true + ) { + return false + } + + // don't verify swift export because we don't have any libraries that use it + if (name == "swiftExportClasspathResolvable") return false + + // don't verify baseline profile generating project dependencies + if (name == "baselineProfile") return false + if (name == "releaseBaselineProfile") return false + + // Only used to run kotlinx benchmarks. Artifacts are not published by this configuration. + if (name == "benchmarkGenerator.resolver") return false + + // don't verify samples + if (name == "samples") return false + + return true +} + +private fun shouldVerifyDependency(dependency: Dependency): Boolean { + // Only verify dependencies within the scope of our versioning policies. + if (dependency.group == null) return false + if (!dependency.group!!.startsWith("androidx.")) return false + if (dependency.name == "annotation-sampled") return false + if (dependency.version == SNAPSHOT_MARKER) { + // This only happens in playground builds where this magic version gets replaced with + // the version from the snapshotBuildId defined in playground-common/playground.properties. + // It is best to leave their validation to the aosp build to ensure it is the right + // version. + return false + } + + return true +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyELFRegionAlignmentTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyELFRegionAlignmentTask.kt new file mode 100644 index 0000000000000..6bd168b9c4bdf --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyELFRegionAlignmentTask.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.TaskAction + +/** + * Task for verifying the ELF regions in all shared libs in androidx are aligned to 16Kb boundary + */ +@CacheableTask +abstract class VerifyELFRegionAlignmentTask : DefaultTask() { + init { + group = "Verification" + description = "Task for verifying alignment in shared libs" + } + + @get:[InputFiles Classpath] + abstract val files: ConfigurableFileCollection + + @TaskAction + fun verifyELFRegionAlignment() { + files.forEach { + val alignment = getELFAlignment(it.path) + check(alignment == "2**14") { + "Expected ELF alignment of 2**14 for file ${it.name}, got $alignment" + } + } + } +} + +private fun getELFAlignment(filePath: String): String? { + val alignment = + ProcessBuilder("objdump", "-p", filePath).start().inputStream.bufferedReader().useLines { + lines -> + lines.filter { it.contains("LOAD") }.map { it.split(" ").last() }.firstOrNull() + } + return alignment +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyLicenseAndVersionFilesTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyLicenseAndVersionFilesTask.kt new file mode 100644 index 0000000000000..000b7b0fcdfe5 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyLicenseAndVersionFilesTask.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.io.File +import java.io.FileInputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** Task for verifying license and version files in Androidx artifacts */ +@CacheableTask +abstract class VerifyLicenseAndVersionFilesTask : DefaultTask() { + @get:[InputDirectory PathSensitive(PathSensitivity.RELATIVE)] + abstract val repositoryDirectory: DirectoryProperty + + @TaskAction + fun verifyFiles() { + verifyVersionFilesPresent() + verifyLicenseFilesPresent() + } + + private fun verifyVersionFilesPresent() { + repositoryDirectory.asFile.get().walk().forEach { file -> + var expectedPrefix = "androidx" + if (file.path.contains("/libyuv/")) + expectedPrefix = "libyuv_libyuv" // external library that we don't publish + if (file.extension == "aar") { + val inputStream = FileInputStream(file) + val aarFileInputStream = ZipInputStream(inputStream) + var entry: ZipEntry? = aarFileInputStream.nextEntry + while (entry != null) { + if (entry.name == "classes.jar") { + var foundVersionFile = false + val classesJarInputStream = ZipInputStream(aarFileInputStream) + var jarEntry = classesJarInputStream.nextEntry + while (jarEntry != null) { + if ( + jarEntry.name.startsWith("META-INF/$expectedPrefix.") && + jarEntry.name.endsWith(".version") + ) { + foundVersionFile = true + break + } + jarEntry = classesJarInputStream.nextEntry + } + if (!foundVersionFile) { + throw Exception( + "Missing classes.jar/META-INF/$expectedPrefix.*version " + + "file in ${file.absolutePath}" + ) + } + break + } + entry = aarFileInputStream.nextEntry + } + } + } + } + + private fun verifyLicenseFilesPresent() { + repositoryDirectory.asFile.get().walk().forEach { file -> + if (file.extension in listOf("aar", "jar", "klib")) { + if (!zipContainsLicense(file)) { + throw Exception( + "Missing META-INF/*/LICENSE.txt or default/licenses/*/LICENSE.txt " + + "file in ${file.absolutePath}" + ) + } + } + } + } + + private fun zipContainsLicense(file: File): Boolean { + val inputStream = FileInputStream(file) + val zipInputStream = ZipInputStream(inputStream) + var entry: ZipEntry? = zipInputStream.nextEntry + while (entry != null) { + if (licensePatterns.any { it.matches(entry.name) }) { + return true + } + entry = zipInputStream.nextEntry + } + return false + } +} + +private val licensePatterns = + listOf(Regex("META-INF/.*/LICENSE.txt"), Regex("default/licenses/.*/LICENSE.txt")) diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyRelocatedDependenciesTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyRelocatedDependenciesTask.kt new file mode 100644 index 0000000000000..3e3d5f3ec4ff4 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VerifyRelocatedDependenciesTask.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.VerifyRelocatedDependenciesTask.Companion.ALLOWED_CONFIGURATIONS +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction + +/** Ensures specified libraries are always relocated/jarjarred */ +@CacheableTask +abstract class VerifyRelocatedDependenciesTask : DefaultTask() { + + @get:Input abstract val allDependencies: ListProperty>> + + @Internal val projectPath: String = project.path + + @Internal val librariesToCheck: List = listOf("protobuf-javalite", "protobuf-java") + + @TaskAction + fun check() { + if (projectPath == ":benchmark:benchmark-baseline-profile-gradle-plugin") { + return + } + val violations = + allDependencies.get().filter { (_, artifacts) -> + librariesToCheck.any { artifacts.contains(it) } + } + + if (violations.isNotEmpty()) { + val message = buildString { + appendLine("The following configurations contain disallowed dependencies:") + violations.forEach { (configurationName, artifacts) -> + appendLine("Configuration: $configurationName") + artifacts.forEach { artifact -> + if (librariesToCheck.contains(artifact)) { + appendLine(" - $artifact") + } + } + } + appendLine( + "Publishing $projectPath is not allowed until the above dependencies are " + + "relocated. Consider using the AndroidXRepackagePlugin." + ) + } + throw GradleException(message) + } + } + + internal companion object { + const val TASK_NAME = "verifyRelocatedDependencies" + val ALLOWED_CONFIGURATIONS = listOf("compileOnly", "repackage") + } +} + +internal fun Project.registerValidateRelocatedDependenciesTask() = + tasks + .register( + VerifyRelocatedDependenciesTask.TASK_NAME, + VerifyRelocatedDependenciesTask::class.java, + ) { + val depsProvider: Provider>>> = + project.providers.provider { + project.configurations + .filter { configuration -> + configuration.isPublished() && + !configuration.isCanBeResolved && + configuration.name !in ALLOWED_CONFIGURATIONS + } + .map { configuration -> + configuration.name to + configuration.allDependencies.map { dependency -> dependency.name } + } + } + it.allDependencies.set(depsProvider) + it.cacheEvenIfNoOutputs() + } + .also { addToBuildOnServer(it) } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt new file mode 100644 index 0000000000000..ab26e3776a116 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/VersionFileWriterTask.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import androidx.build.ProjectLayoutType.Companion.isJetBrainsFork +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import java.io.File +import java.io.PrintWriter +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.androidx.build.JetBrainsPublication +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +/** Task that allows to write a version to a given output file. */ +@DisableCachingByDefault(because = "Doesn't benefit from caching") +abstract class VersionFileWriterTask : DefaultTask() { + @get:Input abstract val version: Property + @get:Input abstract val relativePath: Property + @get:OutputDirectory abstract val outputDir: DirectoryProperty + + /** The main method for actually writing out the file. */ + @TaskAction + fun run() { + val outputFile = File(outputDir.get().asFile, relativePath.get()) + outputFile.parentFile.mkdirs() + val writer = PrintWriter(outputFile) + writer.println(version.get()) + writer.close() + } +} + +/** + * Sets up Android Library project to have a task that generates a version file. + * + * @receiver an Android Library project. + */ +fun Project.configureVersionFileWriter( + libraryAndroidComponentsExtension: LibraryAndroidComponentsExtension, + androidXExtension: AndroidXExtension, +) { + if (isJetBrainsFork(project) && JetBrainsPublication.shouldPublish(this)) return + val writeVersionFile = registerVersionFileTask(androidXExtension) + libraryAndroidComponentsExtension.onVariants { + it.sources.resources!!.addGeneratedSourceDirectory( + writeVersionFile, + VersionFileWriterTask::outputDir, + ) + } +} + +fun Project.configureVersionFileWriter( + kmpExtension: KotlinMultiplatformExtension, + androidXExtension: AndroidXExtension, +) { + if (isJetBrainsFork(project) && JetBrainsPublication.shouldPublish(this)) return + val writeVersionFile = registerVersionFileTask(androidXExtension) + writeVersionFile.configure { + it.outputDir.set(layout.buildDirectory.dir("generatedVersionFile")) + } + val sourceSet = kmpExtension.sourceSets.getByName("androidMain") + val resources = sourceSet.resources + val includes = resources.includes + resources.srcDir(writeVersionFile.map { it.outputDir }) + if (includes.isNotEmpty()) { + includes.add("META-INF/*.version") + resources.setIncludes(includes) + } +} + +private fun Project.registerVersionFileTask( + androidXExtension: AndroidXExtension +): TaskProvider { + val fileNameProvider = provider { String.format("META-INF/%s_%s.version", group, name) } + val versionProvider = + androidXExtension.shouldPublish.map { + if (it) { + version().toString() + } else { + "0.0.0" + } + } + + val shouldPublish = androidXExtension.shouldPublish + + val writeVersionFile = + tasks.register("writeVersionFile", VersionFileWriterTask::class.java) { + it.version.set(versionProvider) + it.relativePath.set(fileNameProvider) + it.onlyIf { + // We only add version file if is a library that is publishing. + shouldPublish.get() + } + } + + return writeVersionFile +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/XmlParser.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/XmlParser.kt new file mode 100644 index 0000000000000..2bf2e8c231a4d --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/XmlParser.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build + +import java.io.StringReader +import java.util.StringTokenizer +import org.apache.xerces.jaxp.SAXParserImpl.JAXPSAXParser +import org.dom4j.Document +import org.dom4j.DocumentException +import org.dom4j.DocumentFactory +import org.dom4j.io.SAXReader +import org.xml.sax.InputSource +import org.xml.sax.XMLReader + +/** Parses an xml string */ +@Throws(DocumentException::class) +internal fun parseXml(text: String, namespaceUris: Map): Document { + val docFactory = DocumentFactory() + docFactory.xPathNamespaceURIs = namespaceUris + // Ensure that we're consistently using JAXP parser. + val xmlReader = JAXPSAXParser() + return parseXml(docFactory, xmlReader, text) +} + +// Copied from org.dom4j.DocumentHelper with modifications to allow SAXReader configuration. +@Throws(DocumentException::class) +private fun parseXml( + documentFactory: DocumentFactory, + xmlReader: XMLReader, + text: String, +): Document { + val reader = SAXReader.createDefault() + reader.documentFactory = documentFactory + reader.xmlReader = xmlReader + val encoding = getEncoding(text) + val source = InputSource(StringReader(text)) + source.encoding = encoding + val result = reader.read(source) + if (result.xmlEncoding == null) { + result.xmlEncoding = encoding + } + return result +} + +// Copied from org.dom4j.DocumentHelper. +private fun getEncoding(text: String): String? { + var result: String? = null + val xml = text.trim { it <= ' ' } + if (xml.startsWith("") + val sub = xml.substring(0, end) + val tokens = StringTokenizer(sub, " =\"'") + while (tokens.hasMoreTokens()) { + val token = tokens.nextToken() + if ("encoding" == token) { + if (tokens.hasMoreTokens()) { + result = tokens.nextToken() + } + break + } + } + } + return result +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt new file mode 100644 index 0000000000000..2370a94909b52 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt @@ -0,0 +1,401 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.binarycompatibilityvalidator + +import androidx.build.AndroidXMultiplatformExtension +import androidx.build.Version +import androidx.build.addToBuildOnServer +import androidx.build.addToCheckTask +import androidx.build.checkapi.ApiType +import androidx.build.checkapi.getBcvFileDirectory +import androidx.build.checkapi.getRequiredCompatibilityApiFileFromDir +import androidx.build.checkapi.shouldWriteVersionedApiFile +import androidx.build.getDistributionDirectory +import androidx.build.getLibraryClasspath +import androidx.build.getSupportRootFolder +import androidx.build.isWriteVersionedApiFilesEnabled +import androidx.build.metalava.UpdateApiTask +import androidx.build.multiplatformExtension +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import androidx.build.version +import com.android.utils.appendCapitalized +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.file.Directory +import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.abi.tools.KlibTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.HostManager + +private const val GENERATE_NAME = "generateAbi" +private const val CHECK_NAME = "checkAbi" +private const val CHECK_RELEASE_NAME = "checkAbiRelease" +private const val UPDATE_NAME = "updateAbi" +private const val IGNORE_CHANGES_NAME = "ignoreAbiChanges" + +private const val KLIB_DUMPS_DIRECTORY = "klib" +private const val NATIVE_SUFFIX = "native" +internal const val CURRENT_API_FILE_NAME = "current.txt" +private const val IGNORE_FILE_NAME = "current.ignore" +private const val ABI_GROUP_NAME = "abi" +private const val CROSS_COMPILATION_FLAG = "kotlin.native.enableKlibsCrossCompilation" + +class BinaryCompatibilityValidation( + val project: Project, + private val kotlinMultiplatformExtension: KotlinMultiplatformExtension, +) { + private val projectVersion: Version = project.version() + + fun setupBinaryCompatibilityValidatorTasks() = + project.afterEvaluate { + val androidXMultiplatformExtension = + project.extensions.getByType(AndroidXMultiplatformExtension::class.java) + if (!androidXMultiplatformExtension.enableBinaryCompatibilityValidator) { + return@afterEvaluate + } + val checkAll: TaskProvider = project.tasks.register(CHECK_NAME) + val updateAll: TaskProvider = project.tasks.register(UPDATE_NAME) + configureKlibTasks(project, checkAll, updateAll) + if (project.multiplatformExtension?.hasUnsupportedTargets() == false) { + project.addToCheckTask(checkAll) + project.addToBuildOnServer(checkAll) + project.tasks.named("updateApi", UpdateApiTask::class.java) { + it.dependsOn(updateAll) + } + } + } + + private fun configureKlibTasks( + project: Project, + checkAll: TaskProvider, + updateAll: TaskProvider, + ) { + if (kotlinMultiplatformExtension.nativeTargets().isEmpty()) { + return + } + val runtimeClasspath: FileCollection = + project.getLibraryClasspath("kotlinCompilerEmbeddable") + val abiToolsClasspath: FileCollection = project.getLibraryClasspath("kotlinAbiTools") + val projectAbiDir = project.getBcvFileDirectory().dir(NATIVE_SUFFIX) + val currentIgnoreFile = projectAbiDir.file(IGNORE_FILE_NAME) + + val klibDumpDir = project.layout.buildDirectory.dir(KLIB_DUMPS_DIRECTORY) + val klibDumpFile = klibDumpDir.map { it.file(CURRENT_API_FILE_NAME) } + + val generateAbi = + project.generateAbiTask( + klibDumpFile, + abiToolsClasspath, + kotlinMultiplatformExtension.hasUnsupportedTargets(), + kotlinMultiplatformExtension.hasCInterop(), + project.providers.gradleProperty(CROSS_COMPILATION_FLAG).get() == "true", + ) + val generatedAndMergedApiFile: Provider = + generateAbi.map { it.abiFile } + val updateKlibAbi = + project.updateKlibAbiTask(projectAbiDir, generatedAndMergedApiFile, runtimeClasspath) + + val checkKlibAbi = + project.checkKlibAbiTask( + projectAbiDir.file(CURRENT_API_FILE_NAME), + generatedAndMergedApiFile, + projectAbiDir, + ) + val checkKlibAbiRelease = + project.checkKlibAbiReleaseTask( + generatedAndMergedApiFile, + projectAbiDir, + currentIgnoreFile, + runtimeClasspath, + ) + + updateKlibAbi.configure { update -> + checkKlibAbiRelease?.let { check -> update.dependsOn(check) } + } + updateAll.configure { it.dependsOn(updateKlibAbi) } + checkAll.configure { checkTask -> + checkTask.dependsOn(checkKlibAbi) + checkKlibAbiRelease?.let { releaseCheck -> checkTask.dependsOn(releaseCheck) } + } + } + + /* Check that the current ABI definition is up to date. */ + private fun Project.checkKlibAbiTask( + projectApiFile: RegularFile, + generatedApiFile: Provider, + projectAbiDir: Directory, + ) = + project.tasks.register( + CHECK_NAME.appendCapitalized(NATIVE_SUFFIX), + CheckAbiEquivalenceTask::class.java, + ) { + it.checkedInDump = projectApiFile + it.builtDump = generatedApiFile + it.projectAbiDir.set(projectAbiDir) + val projectDirPath = + project.projectDir.path.removePrefix(project.getSupportRootFolder().path + "/") + + it.debugOutFile.set( + project.getDistributionDirectory().map { outDir -> + // e.g. out/bcv/foo/bar/bar + outDir.dir("bcv").dir(projectDirPath).file("actual_current.txt") + } + ) + it.group = ABI_GROUP_NAME + it.cacheEvenIfNoOutputs() + it.shouldWriteVersionedAbiFile.set(project.shouldWriteVersionedApiFile()) + it.version.set(projectVersion.toString()) + } + + /* Check that the current ABI definition is compatible with most recently released version */ + private fun Project.checkKlibAbiReleaseTask( + mergedApiFile: Provider, + klibApiDir: Directory, + ignoreFile: RegularFile, + runtimeClasspath: FileCollection, + ) = + project.getRequiredCompatibilityAbiLocation(NATIVE_SUFFIX)?.let { requiredCompatFile -> + val previousApiDump = klibApiDir.file(requiredCompatFile.name) + val referenceVersionProvider = provider { requiredCompatFile.nameWithoutExtension } + project.tasks.register(IGNORE_CHANGES_NAME, IgnoreAbiChangesTask::class.java) { + it.currentApiDump.set(mergedApiFile.map { fileProperty -> fileProperty.get() }) + it.previousApiDump.set(previousApiDump) + it.dependencies.set( + kotlinMultiplatformExtension.nativeTargets().map { target -> + DependenciesForTarget( + KlibTarget.fromKonanTargetName(target.konanTarget.name).targetName, + target.compileDependencyFiles(), + ) + } + ) + it.ignoreFile.set(ignoreFile) + it.runtimeClasspath.from(runtimeClasspath) + it.projectVersion = provider { projectVersion.toString() } + it.referenceVersion = referenceVersionProvider + } + project.tasks.register(CHECK_RELEASE_NAME, CheckAbiIsCompatibleTask::class.java) { + it.dependencies.set( + kotlinMultiplatformExtension.nativeTargets().map { target -> + DependenciesForTarget( + KlibTarget.fromKonanTargetName(target.konanTarget.name).targetName, + target.compileDependencyFiles(), + ) + } + ) + it.currentApiDump.set(mergedApiFile.map { fileProperty -> fileProperty.get() }) + it.previousApiDump.set(previousApiDump) + it.projectVersion = provider { projectVersion.toString() } + it.referenceVersion = referenceVersionProvider + it.ignoreFile.set(ignoreFile) + it.group = ABI_GROUP_NAME + it.runtimeClasspath.from(runtimeClasspath) + it.cacheEvenIfNoOutputs() + } + } + + /* Updates the current abi file as well as the versioned abi file if appropriate */ + private fun Project.updateKlibAbiTask( + klibApiDir: Directory, + mergedKlibFile: Provider, + runtimeClasspath: FileCollection, + ) = + project.tasks.register( + UPDATE_NAME.appendCapitalized(NATIVE_SUFFIX), + UpdateAbiTask::class.java, + ) { + it.outputDir.set(klibApiDir) + it.inputApiLocation.set(mergedKlibFile.map { fileProperty -> fileProperty.get() }) + it.version.set(projectVersion.toString()) + it.shouldWriteVersionedApiFile.set(project.shouldWriteVersionedApiFile()) + it.group = ABI_GROUP_NAME + it.runtimeClasspath.from(runtimeClasspath) + } + + /* Generate ABI dump files in build directory */ + private fun Project.generateAbiTask( + mergeFile: Provider, + runtimeClasspath: FileCollection, + hasUnsupportedTargets: Boolean, + hasCInterop: Boolean, + crossCompilationEnabled: Boolean, + ) = + project.tasks.register(GENERATE_NAME, GenerateAbiTask::class.java) { + // This only affects the external process launched by this task, + // NOT the core Kotlin compilation tasks in the same build. + it.runtimeClasspath.from(runtimeClasspath) + it.abiFile.set(mergeFile) + it.excludedAnnotatedWith.addAll(nonPublicMarkers) + it.klibs.set( + kotlinMultiplatformExtension.nativeTargets().map { target -> + val klibTarget = + KlibTarget.fromKonanTargetName(target.konanTarget.name) + .configureName(target.targetName) + objects.newInstance(KlibTargetInfo::class.java).apply { + targetName = klibTarget.configurableName + canonicalTargetName = klibTarget.targetName + klibFiles = + target.compilations.getByName(MAIN_COMPILATION_NAME).output.classesDirs + } + } + ) + it.group = ABI_GROUP_NAME + it.doFirst { + runHostCompatibilityChecks( + hasUnsupportedTargets, + hasCInterop, + crossCompilationEnabled, + ) + } + } +} + +private fun Project.getRequiredCompatibilityAbiLocation(suffix: String) = + getRequiredCompatibilityApiFileFromDir( + project.getBcvFileDirectory().dir(suffix).asFile, + project.version(), + ApiType.CLASSAPI, + enforceVersionContinuity = isWriteVersionedApiFilesEnabled(), + ) + +private fun KotlinMultiplatformExtension.nativeTargets() = + targets.withType(KotlinNativeTarget::class.java).matching { + it.platformType == KotlinPlatformType.native + } + +private fun KotlinMultiplatformExtension.hasCInterop(): Boolean { + val mainCompilations = nativeTargets().map { it.compilations.getByName(MAIN_COMPILATION_NAME) } + return mainCompilations.any { it.cinterops.isNotEmpty() } +} + +private fun KotlinMultiplatformExtension.hasUnsupportedTargets(): Boolean { + val hostManager = HostManager() + return nativeTargets().any { !hostManager.isEnabled(it.konanTarget) } +} + +private fun runHostCompatibilityChecks( + hasUnsupportedTargets: Boolean, + hasCInterop: Boolean, + crossCompilationEnabled: Boolean, +) { + if (!hasUnsupportedTargets) { + // running on mac, or project has no mac targets. No further checks necessary + return + } + if (hasCInterop) { + // It's impossible to run these tasks on the current host, because they require cinterop + // so cross compilation is not an option + throw GradleException( + """ + Project uses cinterop and cannot be compiled on the current host (${HostManager.host}). + + ABI checks and updates need to compile all targets to run. Please run these tasks on a Mac machine which can build all targets. + """ + ) + } + // Unsupported targets exist, but they can be built by enabling cross compilation just for the + // ABI tasks + if (!crossCompilationEnabled) + throw GradleException( + """ + Project requires cross compilation to be compiled on the current host (${HostManager.host}). + + Please re-run the tasks with cross compilation enabled using the flag '-Pkotlin.native.enableKlibsCrossCompilation=true' + """ + ) +} + +// Not ideal to have a list instead of a pattern to match but this is all the API supports right now +// https://github.com/Kotlin/binary-compatibility-validator/issues/280 +private val nonPublicMarkers = + setOf( + "androidx.annotation.Experimental", + "androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport", + "androidx.benchmark.ExperimentalBenchmarkConfigApi", + "androidx.benchmark.ExperimentalBenchmarkStateApi", + "androidx.benchmark.ExperimentalBlackHoleApi", + "androidx.benchmark.macro.ExperimentalMacrobenchmarkApi", + "androidx.benchmark.macro.ExperimentalMetricApi", + "androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi", + "androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi", + "androidx.camera.core.ExperimentalUseCaseApi", + "androidx.car.app.annotations.ExperimentalCarApi", + "androidx.compose.animation.ExperimentalAnimationApi", + "androidx.compose.animation.ExperimentalSharedTransitionApi", + "androidx.compose.animation.core.ExperimentalAnimatableApi", + "androidx.compose.animation.core.ExperimentalAnimationSpecApi", + "androidx.compose.animation.core.ExperimentalTransitionApi", + "androidx.compose.animation.core.InternalAnimationApi", + "androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", + "androidx.compose.foundation.gestures.ExperimentalTapGestureDetectorBehaviorApi", + "androidx.compose.foundation.ExperimentalFoundationApi", + "androidx.compose.foundation.InternalFoundationApi", + "androidx.compose.foundation.layout.ExperimentalLayoutApi", + "androidx.compose.material.ExperimentalMaterialApi", + "androidx.compose.runtime.ExperimentalComposeApi", + "androidx.compose.runtime.ExperimentalComposeRuntimeApi", + "androidx.compose.runtime.InternalComposeApi", + "androidx.compose.runtime.InternalComposeTracingApi", + "androidx.compose.ui.ExperimentalComposeUiApi", + "androidx.compose.ui.ExperimentalIndirectTouchTypeApi", + "androidx.compose.ui.InternalComposeUiApi", + "androidx.compose.ui.input.pointer.util.ExperimentalVelocityTrackerApi", + "androidx.compose.ui.node.InternalCoreApi", + "androidx.compose.ui.test.ExperimentalTestApi", + "androidx.compose.ui.test.InternalTestApi", + "androidx.compose.ui.text.ExperimentalTextApi", + "androidx.compose.ui.text.InternalTextApi", + "androidx.compose.ui.unit.ExperimentalUnitApi", + "androidx.constraintlayout.compose.ExperimentalMotionApi", + "androidx.core.telecom.util.ExperimentalAppActions", + "androidx.credentials.ExperimentalDigitalCredentialApi", + "androidx.glance.ExperimentalGlanceApi", + "androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi", + "androidx.health.connect.client.ExperimentalDeduplicationApi", + "androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi", + "androidx.ink.authoring.ExperimentalLatencyDataApi", + "androidx.ink.brush.ExperimentalInkCustomBrushApi", + "androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi", + "androidx.paging.ExperimentalPagingApi", + "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.RegisterSourceOptIn", + "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.Ext8OptIn", + "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.Ext10OptIn", + "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.Ext11OptIn", + "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.Ext12OptIn", + "androidx.room3.ExperimentalRoomApi", + "androidx.room3.compiler.processing.ExperimentalProcessingApi", + "androidx.tv.foundation.ExperimentalTvFoundationApi", + "androidx.wear.compose.foundation.ExperimentalWearFoundationApi", + "androidx.wear.compose.material.ExperimentalWearMaterialApi", + "androidx.window.core.ExperimentalWindowApi", + "androidx.compose.material3.ExperimentalMaterial3Api", + ) + +const val NEW_ISSUE_URL = "https://b.corp.google.com/issues/new?component=1102332" + +private fun KotlinNativeTarget.compileDependencyFiles(): FileCollection = + compilations.getByName(MAIN_COMPILATION_NAME).compileDependencyFiles.filter { + // stdlib is a klib directory so no extension + it.extension == "" || it.extension == "klib" + } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckAbiEquivalenceTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckAbiEquivalenceTask.kt new file mode 100644 index 0000000000000..f04a1e0ea671e --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckAbiEquivalenceTask.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.binarycompatibilityvalidator + +import androidx.build.metalava.summarizeDiff +import org.apache.commons.io.FileUtils +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.jetbrains.kotlin.konan.target.HostManager + +/** Compares two ABI txt files against each other to confirm they are equal */ +@CacheableTask +abstract class CheckAbiEquivalenceTask : DefaultTask() { + + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFile + abstract var checkedInDump: RegularFile + + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFile + abstract var builtDump: Provider + + @get:Input abstract val shouldWriteVersionedAbiFile: Property + @get:Input abstract val version: Property + + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputDirectory + abstract val projectAbiDir: DirectoryProperty + + @get:OutputFile abstract val debugOutFile: RegularFileProperty + + @TaskAction + fun execute() { + if (shouldWriteVersionedAbiFile.get()) { + val versionedFile = projectAbiDir.get().asFile.resolve("${version.get()}.txt") + if (!versionedFile.exists()) { + throw GradleException("Missing versioned abi file: ${versionedFile.path}") + } + } + checkEqual() + } + + private fun checkEqual() { + val expected = checkedInDump.asFile + val actual = builtDump.get().asFile.get() + val debugOutFile = debugOutFile.get().asFile + if (!FileUtils.contentEquals(expected, actual)) { + if (HostManager.hostIsMac) { + actual.copyTo(debugOutFile, overwrite = true) + } + val diff = summarizeDiff(expected, actual) + val messageBuilder = StringBuilder() + messageBuilder.append( + """ + ABI definition has changed + + Declared definition is $expected + True definition is $actual + + Please run `./gradlew updateAbi` to confirm these changes are + intentional by updating the ABI definition. + """ + ) + if (HostManager.hostIsMac) { + messageBuilder.append( + """ + + Actual output file has been written to ${debugOutFile.path}. + If you are unable to generate the dump file for all targets locally you can copy the definition from the expected output file created during presubmit. + """ + .trimIndent() + ) + } + messageBuilder.append( + """ + + Difference between these files: + $diff""${'"'} + """ + .trimIndent() + ) + throw GradleException(messageBuilder.toString()) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckAbiIsCompatibleTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckAbiIsCompatibleTask.kt new file mode 100644 index 0000000000000..6973d529d9bbd --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/CheckAbiIsCompatibleTask.kt @@ -0,0 +1,186 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.binarycompatibilityvalidator + +import androidx.binarycompatibilityvalidator.BinaryCompatibilityChecker +import androidx.binarycompatibilityvalidator.KlibDumpParser +import androidx.binarycompatibilityvalidator.ValidationException +import androidx.build.Version +import androidx.build.logging.TERMINAL_RED +import androidx.build.logging.TERMINAL_RESET +import androidx.build.metalava.shouldFreezeApis +import androidx.build.metalava.summarizeDiff +import java.io.File +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader + +class DependenciesForTarget( + @get:Input val targetName: String, + @get:PathSensitive(PathSensitivity.NONE) @get:InputFiles val files: FileCollection, +) + +@CacheableTask +abstract class CheckAbiIsCompatibleTask +@Inject +constructor(@Internal protected val workerExecutor: WorkerExecutor) : DefaultTask() { + + // Input annotation is handled by getIgnoreFile + @get:Internal abstract val ignoreFile: RegularFileProperty + + /** Text file from which API signatures will be read. */ + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFile + abstract val previousApiDump: RegularFileProperty + + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFile + abstract val currentApiDump: RegularFileProperty + + @get:Input abstract var referenceVersion: Provider + + @get:Input abstract var projectVersion: Provider + + @PathSensitive(PathSensitivity.RELATIVE) + @InputFile + @Optional + fun getBaseline(): File? = ignoreFile.get().asFile.takeIf { it.exists() } + + @get:Classpath abstract val runtimeClasspath: ConfigurableFileCollection + + @get:Nested abstract val dependencies: ListProperty + + @TaskAction + fun execute() { + val (previousApiPath, previousApiDumpText) = + previousApiDump.get().asFile.let { it.path to it.readText() } + val (currentApiPath, currentApiDumpText) = + currentApiDump.get().asFile.let { it.path to it.readText() } + val shouldFreeze = + shouldFreezeApis(Version(referenceVersion.get()), Version(projectVersion.get())) + + // Execute BCV code as a WorkAction to allow setting the classpath for the action. + // This is to work around the kotlin compiler needing to be a compileOnly dependency for + // buildSrc (https://kotl.in/gradle/internal-compiler-symbols, aosp/3368960). + val workQueue = workerExecutor.classLoaderIsolation { it.classpath.from(runtimeClasspath) } + workQueue.submit(CheckCompatibilityWorker::class.java) { params -> + params.previousApiDumpText.set(previousApiDumpText) + params.previousApiPath.set(previousApiPath) + params.currentApiDumpText.set(currentApiDumpText) + params.currentApiPath.set(currentApiPath) + params.baseline.set(ignoreFile) + params.shouldFreeze.set(shouldFreeze) + params.referenceVersion.set(referenceVersion) + params.dependencies.set( + dependencies.get().associate { it.targetName to it.files.files } + ) + } + } +} + +private interface CheckCompatibilityParameters : WorkParameters { + val previousApiDumpText: Property + val previousApiPath: Property + val currentApiDumpText: Property + val currentApiPath: Property + val baseline: RegularFileProperty + val referenceVersion: Property + val shouldFreeze: Property + val dependencies: MapProperty> +} + +private abstract class CheckCompatibilityWorker : WorkAction { + @OptIn(ExperimentalLibraryAbiReader::class) + override fun execute() { + val previousDump = + KlibDumpParser(parameters.previousApiDumpText.get(), parameters.previousApiPath.get()) + .parse() + val currentDump = + KlibDumpParser(parameters.currentApiDumpText.get(), parameters.currentApiPath.get()) + .parse() + + try { + BinaryCompatibilityChecker.checkAllBinariesAreCompatible( + currentDump, + previousDump, + parameters.baseline.get().asFile.takeIf { it.exists() }, + validate = true, + shouldFreeze = parameters.shouldFreeze.get(), + dependencies = parameters.dependencies.get(), + ) + } catch (e: ValidationException) { + if (parameters.shouldFreeze.get()) { + throw GradleException( + frozenApiErrorMessage( + parameters.referenceVersion.get(), + previousAbiDump = File(parameters.previousApiPath.get()), + currentAbiDump = File(parameters.currentApiPath.get()), + ) + ) + } + throw GradleException(compatErrorMessage(e), e) + } + } + + private fun compatErrorMessage(validationException: ValidationException) = + """ +${TERMINAL_RED}Your change has binary compatibility issues. Please resolve them before updating.$TERMINAL_RESET + +${validationException.message} + +If you *intentionally* want to break compatibility, you can suppress it with +./gradlew ignoreAbiChanges && ./gradlew updateAbi + +If you believe these changes are actually compatible and that this is a tooling error, please file a bug. $NEW_ISSUE_URL +""" + + private fun frozenApiErrorMessage( + referenceVersion: String, + previousAbiDump: File, + currentAbiDump: File, + ) = + """ +${TERMINAL_RED}The ABI surface was finalized in $referenceVersion. Revert the changes unless you have permission from Android API Council.$TERMINAL_RESET + +${summarizeDiff(previousAbiDump,currentAbiDump)} + +If you have obtained permission from Android API Council or Jetpack Working Group to bypass this policy, you can suppress this check with: +./gradlew ignoreAbiChanges && ./gradlew updateAbi +""" +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/GenerateAbiTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/GenerateAbiTask.kt new file mode 100644 index 0000000000000..477cd2f663034 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/GenerateAbiTask.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.binarycompatibilityvalidator + +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.abi.tools.AbiFilters +import org.jetbrains.kotlin.abi.tools.AbiTools +import org.jetbrains.kotlin.abi.tools.KlibTarget + +@CacheableTask +abstract class GenerateAbiTask +@Inject +constructor(@Internal protected val workerExecutor: WorkerExecutor) : DefaultTask() { + @get:OutputFile abstract val abiFile: RegularFileProperty + + @get:Nested internal abstract val klibs: ListProperty + + @get:[Input Optional] + abstract val excludedAnnotatedWith: SetProperty + + @get:Classpath abstract val runtimeClasspath: ConfigurableFileCollection + + @TaskAction + fun execute() { + // Execute BCV code as a WorkAction to allow setting the classpath for the action. + // This is to work around the kotlin compiler needing to be a compileOnly dependency for + // buildSrc (https://kotl.in/gradle/internal-compiler-symbols, aosp/3368960). + val workQueue = workerExecutor.classLoaderIsolation { it.classpath.from(runtimeClasspath) } + workQueue.submit(KlibDumpWorker::class.java) { params -> + params.mergedApiFile.set(abiFile) + params.klibs.set(klibs) + params.excludedAnnotatedWith.set(excludedAnnotatedWith) + } + } +} + +abstract class KlibDumpWorker : WorkAction { + internal interface Parameters : WorkParameters { + @get:OutputFile abstract val mergedApiFile: RegularFileProperty + + @get:Nested abstract val klibs: ListProperty + + @get:[Input Optional] + abstract val excludedAnnotatedWith: SetProperty + } + + private val abiTools = AbiTools.getInstance() + + override fun execute() { + val klibTargets = parameters.klibs.get() + + val filters = + AbiFilters( + includedClasses = emptySet(), + excludedClasses = emptySet(), + includedAnnotatedWith = emptySet(), + parameters.excludedAnnotatedWith.getOrElse(mutableSetOf()), + ) + val mergedDump = abiTools.createKlibDump() + klibTargets.forEach { suite -> + val klibDir = suite.klibFiles.files.first() + if (klibDir.exists()) { + val dump = + abiTools.extractKlibAbi( + klibDir, + KlibTarget(suite.canonicalTargetName, suite.targetName), + filters, + ) + mergedDump.merge(dump) + } + } + mergedDump.print(parameters.mergedApiFile.get().asFile) + } +} + +internal abstract class KlibTargetInfo { + @get:Input abstract var targetName: String + + @get:Input abstract var canonicalTargetName: String + + @get:InputFiles + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract var klibFiles: FileCollection +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/IgnoreAbiChangesTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/IgnoreAbiChangesTask.kt new file mode 100644 index 0000000000000..6ccbfb0db6c1a --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/IgnoreAbiChangesTask.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.build.binarycompatibilityvalidator + +import androidx.binarycompatibilityvalidator.BinaryCompatibilityChecker +import androidx.binarycompatibilityvalidator.KlibDumpParser +import androidx.build.Version +import androidx.build.metalava.shouldFreezeApis +import java.io.File +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader + +@CacheableTask +abstract class IgnoreAbiChangesTask +@Inject +constructor(@Internal protected val workerExecutor: WorkerExecutor) : DefaultTask() { + /** Text file from which API signatures will be read. */ + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFile + abstract val previousApiDump: RegularFileProperty + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFile + abstract val currentApiDump: RegularFileProperty + @get:OutputFile abstract val ignoreFile: RegularFileProperty + @get:Classpath abstract val runtimeClasspath: ConfigurableFileCollection + @get:Input abstract var referenceVersion: Provider + @get:Input abstract var projectVersion: Provider + @get:Nested abstract val dependencies: ListProperty + + @TaskAction + fun execute() { + // Execute BCV code as a WorkAction to allow setting the classpath for the action. + // This is to work around the kotlin compiler needing to be a compileOnly dependency for + // buildSrc (https://kotl.in/gradle/internal-compiler-symbols, aosp/3368960). + val workQueue = workerExecutor.classLoaderIsolation { it.classpath.from(runtimeClasspath) } + workQueue.submit(IgnoreChangesWorker::class.java) { params -> + params.previousApiDump.set(previousApiDump) + params.currentApiDump.set(currentApiDump) + params.ignoreFile.set(ignoreFile) + params.referenceVersion.set(referenceVersion.get()) + params.projectVersion.set(projectVersion.get()) + params.dependencies.set( + dependencies.get().associate { it.targetName to it.files.files } + ) + } + } +} + +private interface IgnoreChangesParameters : WorkParameters { + val previousApiDump: RegularFileProperty + val currentApiDump: RegularFileProperty + val ignoreFile: RegularFileProperty + val referenceVersion: Property + val projectVersion: Property + val dependencies: MapProperty> +} + +private abstract class IgnoreChangesWorker : WorkAction { + @OptIn(ExperimentalLibraryAbiReader::class) + override fun execute() { + val previousDump = KlibDumpParser(parameters.previousApiDump.get().asFile).parse() + val currentDump = KlibDumpParser(parameters.currentApiDump.get().asFile).parse() + val shouldFreeze = + shouldFreezeApis( + Version(parameters.referenceVersion.get()), + Version(parameters.projectVersion.get()), + ) + val ignoredErrors = + BinaryCompatibilityChecker.checkAllBinariesAreCompatible( + currentDump, + previousDump, + null, + validate = false, + shouldFreeze = shouldFreeze, + dependencies = parameters.dependencies.get(), + ) + .map { it.toString() } + .toSet() + parameters.ignoreFile.get().asFile.apply { + if (ignoredErrors.isEmpty()) { + takeIf { exists() }?.delete() + } else { + takeUnless { exists() }?.createNewFile() + writeText(FORMAT_STRING + "\n" + ignoredErrors.joinToString("\n")) + } + } + } + + private companion object { + const val BASELINE_FORMAT_VERSION = "1.0" + const val FORMAT_STRING = "// Baseline format: $BASELINE_FORMAT_VERSION" + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt new file mode 100644 index 0000000000000..49aeb90a3c955 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.binarycompatibilityvalidator + +import androidx.binarycompatibilityvalidator.KlibDumpParser +import androidx.binarycompatibilityvalidator.ParseException +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader + +@CacheableTask +abstract class UpdateAbiTask +@Inject +constructor(@Internal protected val workerExecutor: WorkerExecutor) : DefaultTask() { + + @get:Inject abstract val fileSystemOperations: FileSystemOperations + + @get:Input abstract val version: Property + + @get:Input abstract val shouldWriteVersionedApiFile: Property + + @get:Input abstract val unsupportedNativeTargetNames: ListProperty + + /** Text file from which API signatures will be read. */ + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFile + abstract val inputApiLocation: RegularFileProperty + + /** Directory to which API signatures will be written. */ + @get:OutputDirectory abstract val outputDir: DirectoryProperty + + @get:Classpath abstract val runtimeClasspath: ConfigurableFileCollection + + @TaskAction + fun execute() { + unsupportedNativeTargetNames.get().let { targets -> + if (targets.isNotEmpty()) { + throw GradleException( + "Cannot update API files because the current host doesn't support the " + + "following targets: ${targets.joinToString(", ")}" + ) + } + } + fileSystemOperations.copy { + it.from(inputApiLocation) + it.into(outputDir) + } + if (shouldWriteVersionedApiFile.get()) { + fileSystemOperations.copy { + it.from(inputApiLocation) + it.into(outputDir) + it.rename(CURRENT_API_FILE_NAME, "${version.get()}.txt") + } + } + + // Execute BCV code as a WorkAction to allow setting the classpath for the action. + // This is to work around the kotlin compiler needing to be a compileOnly dependency for + // buildSrc (https://kotl.in/gradle/internal-compiler-symbols, aosp/3368960). + val workQueue = workerExecutor.classLoaderIsolation { it.classpath.from(runtimeClasspath) } + workQueue.submit(UpdateAbiWorker::class.java) { params -> + params.abiFile.set(outputDir.file("current.txt")) + } + } +} + +private interface UpdateAbiParameters : WorkParameters { + val abiFile: RegularFileProperty +} + +private abstract class UpdateAbiWorker : WorkAction { + @OptIn(ExperimentalLibraryAbiReader::class) + override fun execute() { + try { + KlibDumpParser(parameters.abiFile.get().asFile).parse() + } catch (e: ParseException) { + System.err.println( + "Successfully updated API file but parser was unable to parse the generated output. " + + "This is a bug in the parser and should be filed to $NEW_ISSUE_URL" + ) + e.printStackTrace() + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt new file mode 100644 index 0000000000000..c612a0b75278d --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.buildInfo + +import androidx.build.AGGREGATE_BUILD_INFO_FILE_NAME +import androidx.build.buildInfo.CreateAggregateLibraryBuildInfoFileTask.Companion.CREATE_AGGREGATE_BUILD_INFO_FILES_TASK +import androidx.build.getDistributionDirectory +import androidx.build.jetpad.LibraryBuildInfoFile +import com.google.gson.Gson +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** Task for a json file of all dependencies for each artifactId */ +@DisableCachingByDefault(because = "Not worth caching") +abstract class CreateAggregateLibraryBuildInfoFileTask : DefaultTask() { + init { + group = "Help" + description = "Generates a file containing library build information serialized to json" + } + + /** List of each build_info.txt file for each project. */ + @get:Input abstract val libraryBuildInfoFiles: ListProperty + + @get:OutputFile abstract val outputFileProvider: RegularFileProperty + + private data class AllLibraryBuildInfoFiles(val artifacts: ArrayList) + + /** Reads in file and checks that json is valid */ + private fun jsonFileIsValid(jsonFile: File, artifactList: MutableList): Boolean { + if (!jsonFile.exists()) { + return false + } + val gson = Gson() + val jsonString: String = jsonFile.readText(Charsets.UTF_8) + val aggregateBuildInfoFile = gson.fromJson(jsonString, AllLibraryBuildInfoFiles::class.java) + aggregateBuildInfoFile.artifacts.forEach { artifact -> + if (!artifactList.contains("${artifact.groupId}_${artifact.artifactId}")) { + println("Failed to find ${artifact.artifactId} in artifact list!") + return false + } + } + return true + } + + /** + * Create the output file to contain the final complete AndroidX project build info graph file. + * Iterate through the list of project-specific build info files, and collects all dependencies + * as a JSON string. Finally, write this complete dependency graph to a text file as a json list + * of every project's build information + */ + @TaskAction + fun createAndroidxAggregateBuildInfoFile() { + // Loop through each file in the list of libraryBuildInfoFiles and collect all build info + // data from each of these $groupId-$artifactId-_build_info.txt files + val output = StringBuilder() + output.append("{ \"artifacts\": [\n") + val artifactList = mutableListOf() + val outputFile = outputFileProvider.get().asFile + for (infoFile in libraryBuildInfoFiles.get()) { + if ( + (infoFile.isFile and (infoFile.name != outputFile.name)) and + (infoFile.name.contains("_build_info.txt")) + ) { + val fileText: String = infoFile.readText(Charsets.UTF_8) + output.append("$fileText,") + artifactList.add(infoFile.name.replace("_build_info.txt", "")) + } + } + // Remove final ',' from list (so a null object doesn't get added to the end of the list) + output.setLength(output.length - 1) + output.append("]}") + outputFile.writeText(output.toString(), Charsets.UTF_8) + if (!jsonFileIsValid(outputFile, artifactList)) { + throw RuntimeException("JSON written to $outputFile was invalid.") + } + } + + companion object { + const val CREATE_AGGREGATE_BUILD_INFO_FILES_TASK = "createAggregateBuildInfoFiles" + } +} + +fun Project.addTaskToAggregateBuildInfoFileTask(task: Provider) { + rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure { it -> + val aggregateLibraryBuildInfoFileTask = it as CreateAggregateLibraryBuildInfoFileTask + aggregateLibraryBuildInfoFileTask.libraryBuildInfoFiles.add( + task.flatMap { task -> task.outputFile.asFile } + ) + aggregateLibraryBuildInfoFileTask.outputFileProvider.set( + project.getDistributionDirectory().file(AGGREGATE_BUILD_INFO_FILE_NAME) + ) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt new file mode 100644 index 0000000000000..a49de20f6e690 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt @@ -0,0 +1,579 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.buildInfo + +import androidx.build.AndroidXExtension +import androidx.build.AndroidXMultiplatformExtension +import androidx.build.LibraryGroup +import androidx.build.PlatformGroup +import androidx.build.PlatformIdentifier +import androidx.build.addToBuildOnServer +import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask.Companion.TASK_NAME +import androidx.build.docs.CheckTipOfTreeDocsTask.Companion.requiresDocs +import androidx.build.getBuildInfoDirectory +import androidx.build.getProjectZipPath +import androidx.build.getSupportRootFolder +import androidx.build.gitclient.getHeadShaProvider +import androidx.build.jetpad.LibraryBuildInfoFile +import androidx.build.kotlinExtensionOrNull +import com.android.build.api.variant.AndroidComponentsExtension +import com.google.common.annotations.VisibleForTesting +import com.google.gson.GsonBuilder +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.DependencyConstraint +import org.gradle.api.artifacts.ModuleVersionIdentifier +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.component.ComponentWithCoordinates +import org.gradle.api.component.ComponentWithVariants +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency +import org.gradle.api.internal.artifacts.dependencies.DefaultProjectDependencyConstraint +import org.gradle.api.internal.artifacts.ivyservice.projectmodule.ProjectComponentPublication +import org.gradle.api.internal.component.SoftwareComponentInternal +import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.SetProperty +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.configure +import org.gradle.plugin.devel.GradlePluginDevelopmentExtension +import org.gradle.plugin.devel.plugins.JavaGradlePluginPlugin +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion + +/** + * This task generates a library build information file containing the artifactId, groupId, and + * version of public androidx dependencies and release checklist of the library for consumption by + * the Jetpack Release Service (JetPad). + * + * Example: If this task is configured + * - for a project with group name "myGroup" + * - on a variant with artifactId "myArtifact", + * - and root project outDir is "out" + * - and environment variable DIST_DIR is not set + * + * then the build info file will be written to + * "out/dist/build-info/myGroup_myArtifact_build_info.txt" + */ +@DisableCachingByDefault(because = "uses git sha as input") +abstract class CreateLibraryBuildInfoFileTask : DefaultTask() { + init { + group = "Help" + description = "Generates a file containing library build information serialized to json" + } + + @get:OutputFile abstract val outputFile: RegularFileProperty + + @get:Input abstract val artifactId: Property + + @get:Input abstract val groupId: Property + + @get:Input abstract val version: Property + + @get:Optional @get:Input abstract val kotlinVersion: Property + + @get:Input abstract val projectDir: Property + + @get:Input abstract val commit: Property + + @get:Input abstract val groupIdRequiresSameVersion: Property + + @get:Input abstract val projectZipPath: Property + + @get:[Input Optional] + abstract val dependencyList: ListProperty + + @get:[Input Optional] + abstract val allDependencies: ListProperty + + @get:[Input Optional] + abstract val dependencyConstraintList: ListProperty + + @get:[Input Optional] + abstract val testModuleNames: SetProperty + + /** the local project directory without the full framework/support root directory path */ + @get:Input abstract val projectSpecificDirectory: Property + + /** Whether the project should be included in docs-public/build.gradle. */ + @get:Input abstract val shouldPublishDocs: Property + + /** Whether the artifact is from a KMP project. */ + @get:Input abstract val kmp: Property + + /** The project's build target */ + @get:Input abstract val target: Property + + /** The list of KMP artifact children */ + @get:[Input Optional] + abstract val kmpChildren: SetProperty + + /** The list Gradle plugin IDs */ + @get:[Input Optional] + abstract val gradlePluginIds: SetProperty + + private fun writeJsonToFile(info: LibraryBuildInfoFile) { + val resolvedOutputFile: File = outputFile.get().asFile + val outputDir = resolvedOutputFile.parentFile + if (!outputDir.exists()) { + if (!outputDir.mkdirs()) { + throw RuntimeException("Failed to create output directory: $outputDir") + } + } + if (!resolvedOutputFile.exists()) { + if (!resolvedOutputFile.createNewFile()) { + throw RuntimeException( + "Failed to create output dependency dump file: $resolvedOutputFile" + ) + } + } + + // Create json object from the artifact instance + val gson = GsonBuilder().serializeNulls().setPrettyPrinting().create() + val serializedInfo: String = gson.toJson(info) + resolvedOutputFile.writeText(serializedInfo) + } + + private fun resolveAndCollectDependencies(): LibraryBuildInfoFile { + val libraryBuildInfoFile = LibraryBuildInfoFile() + libraryBuildInfoFile.artifactId = artifactId.get() + libraryBuildInfoFile.groupId = groupId.get() + libraryBuildInfoFile.version = version.get() + libraryBuildInfoFile.path = projectDir.get() + libraryBuildInfoFile.sha = commit.get() + libraryBuildInfoFile.groupIdRequiresSameVersion = groupIdRequiresSameVersion.get() + libraryBuildInfoFile.projectZipPath = projectZipPath.get() + libraryBuildInfoFile.kotlinVersion = kotlinVersion.orNull + libraryBuildInfoFile.checks = ArrayList() + libraryBuildInfoFile.dependencies = + if (dependencyList.isPresent) ArrayList(dependencyList.get()) else ArrayList() + libraryBuildInfoFile.allDependencies = + if (allDependencies.isPresent) ArrayList(allDependencies.get()) else ArrayList() + libraryBuildInfoFile.dependencyConstraints = + if (dependencyConstraintList.isPresent) ArrayList(dependencyConstraintList.get()) + else ArrayList() + libraryBuildInfoFile.shouldPublishDocs = shouldPublishDocs.get() + libraryBuildInfoFile.isKmp = kmp.get() + libraryBuildInfoFile.target = target.get() + libraryBuildInfoFile.kmpChildren = + if (kmpChildren.isPresent) kmpChildren.get() else emptySet() + libraryBuildInfoFile.testModuleNames = + if (testModuleNames.isPresent) testModuleNames.get() else emptySet() + libraryBuildInfoFile.gradlePluginIds = + if (gradlePluginIds.isPresent) gradlePluginIds.get() else emptySet() + return libraryBuildInfoFile + } + + /** + * Task: createLibraryBuildInfoFile Iterates through each configuration of the project and + * builds the set of all dependencies. Then adds each dependency to the Artifact class as a + * project or prebuilt dependency. Finally, writes these dependencies to a json file as a json + * object. + */ + @TaskAction + fun createLibraryBuildInfoFile() { + val resolvedArtifact = resolveAndCollectDependencies() + writeJsonToFile(resolvedArtifact) + } + + companion object { + const val TASK_NAME = "createLibraryBuildInfoFiles" + + fun setup( + project: Project, + mavenGroup: LibraryGroup?, + variant: VariantPublishPlan, + shaProvider: Provider, + shouldPublishDocs: Provider, + isKmp: Boolean, + target: String, + kmpChildren: Set, + testModuleNames: Provider>, + gradlePluginIds: Set, + ): TaskProvider { + return project.tasks.register( + TASK_NAME + variant.taskSuffix, + CreateLibraryBuildInfoFileTask::class.java, + ) { task -> + val group = project.group.toString() + val artifactId = variant.artifactId + task.outputFile.set( + project.getBuildInfoDirectory().map { + it.file("${group}_${artifactId.get()}_build_info.txt") + } + ) + task.artifactId.set(artifactId) + task.groupId.set(group) + task.version.set(project.version.toString()) + task.kotlinVersion.set(project.getKotlinPluginVersion()) + task.projectDir.set( + project.projectDir.absolutePath.removePrefix( + project.getSupportRootFolder().absolutePath + ) + ) + task.commit.set(shaProvider) + task.groupIdRequiresSameVersion.set(mavenGroup?.requireSameVersion ?: false) + task.projectZipPath.set(project.getProjectZipPath()) + + // Note: + // `project.projectDir.toString().removePrefix(project.rootDir.toString())` + // does not work because the project rootDir is not guaranteed to be a + // substring of the projectDir + task.projectSpecificDirectory.set( + project.projectDir.absolutePath.removePrefix( + project.getSupportRootFolder().absolutePath + ) + ) + + // lazily compute the task dependency list based on the variant dependencies. + task.dependencyList.set(variant.dependencies.map { it.asBuildInfoDependencies() }) + task.dependencyConstraintList.set( + variant.dependencyConstraints.map { it.asBuildInfoDependencies() } + ) + task.allDependencies.set( + variant.runtimeConfigurationNames.map { configList -> + val deps = LinkedHashSet() + configList.forEach { config -> + project.configurations.named(config).configure { + deps += collectResolvedModules(it) + } + } + deps.sortedWith( + compareBy({ it.groupId }, { it.artifactId }, { it.version }) + ) + } + ) + task.shouldPublishDocs.set(shouldPublishDocs) + task.kmp.set(isKmp) + task.target.set(target) + task.kmpChildren.set(kmpChildren) + task.gradlePluginIds.set(gradlePluginIds) + + // We only want test module names for the parent build info file for Gradle projects + // that have multiple build info files, like KMP. + if (variant.taskSuffix.isBlank()) { + task.testModuleNames.set(testModuleNames) + } + } + } + + fun List.asBuildInfoDependencies() = + filter { it.group.isAndroidXDependency() } + .map { + LibraryBuildInfoFile.Dependency().apply { + this.artifactId = it.name + this.groupId = it.group!! + this.version = it.version!! + this.isTipOfTree = + it is ProjectDependency || it is BuildInfoVariantDependency + } + } + .toHashSet() + .sortedWith(compareBy({ it.groupId }, { it.artifactId }, { it.version })) + + @JvmName("dependencyConstraintsasBuildInfoDependencies") + fun List.asBuildInfoDependencies() = + filter { it.group.isAndroidXDependency() } + .map { + LibraryBuildInfoFile.Dependency().apply { + this.artifactId = it.name + this.groupId = it.group + this.version = it.version!! + this.isTipOfTree = it is DefaultProjectDependencyConstraint + } + } + .toHashSet() + .sortedWith(compareBy({ it.groupId }, { it.artifactId }, { it.version })) + + private fun String?.isAndroidXDependency() = + this != null && + startsWith("androidx.") && + !startsWith("androidx.test") && + !startsWith("androidx.databinding") && + !startsWith("androidx.media3") + + private fun collectResolvedModules( + conf: Configuration + ): Set { + val deps = LinkedHashSet() + val rootComponent = conf.incoming.resolutionResult.root + conf.incoming.resolutionResult.allComponents.forEach { comp -> + // Skip the current project itself + if (comp == rootComponent) return@forEach + when (val id = comp.id) { + is ModuleComponentIdentifier -> { + deps += + LibraryBuildInfoFile.Dependency().apply { + artifactId = id.module + groupId = id.group + version = comp.moduleVersion?.version ?: id.version + isTipOfTree = false + } + } + is ProjectComponentIdentifier -> { + comp.moduleVersion?.let { + deps += + LibraryBuildInfoFile.Dependency().apply { + artifactId = it.name + groupId = it.group + version = it.version + isTipOfTree = true + } + } + } + } + } + return deps + } + } +} + +// Tasks that create a json files of a project's variant's dependencies +fun Project.addCreateLibraryBuildInfoFileTasks( + androidXExtension: AndroidXExtension, + androidXKmpExtension: AndroidXMultiplatformExtension, +) { + androidXExtension.ifReleasing { + val anchorTask = tasks.register("${TASK_NAME}Anchor") + addToBuildOnServer(anchorTask) + configure { + + /** + * Select the appropriate target based on if the project targets any Apple platforms + * + * If the project targets any Apple platform then the project can only be built on the + * 'androidx_multiplatform_mac' target. Otherwise the 'androidx' build target is used. + */ + val buildTarget = + if (hasApplePlatform(androidXKmpExtension.supportedPlatforms)) { + "androidx_multiplatform_mac" + } else { + "androidx" + } + + // Unfortunately, dependency information is only available through internal API + // (See https://github.com/gradle/gradle/issues/21345). + publications.withType(MavenPublicationInternal::class.java).configureEach { mavenPub -> + // java-gradle-plugin creates marker publications that are aliases of the + // main publication. We do not track these aliases. + if (!mavenPub.isAlias) { + createTaskForComponent( + anchorTask = anchorTask, + pub = mavenPub, + libraryGroup = androidXExtension.mavenGroup, + // `mavenPub.artifactId` is a var annotated @ToBeReplacedByLazyProperty + // It may not yet be set to the right value at configuration time, so wrap + // it in a provider. + artifactId = project.provider { mavenPub.artifactId }, + shouldPublishDocs = androidXExtension.requiresDocs(), + isKmp = androidXKmpExtension.supportedPlatforms.isNotEmpty(), + buildTarget = buildTarget, + kmpChildren = androidXKmpExtension.supportedPlatforms.map { it.id }.toSet(), + testModuleNames = androidXExtension.testModuleNames, + isolatedProjectEnabled = androidXExtension.isIsolatedProjectsEnabled(), + variantName = mavenPub.name, + ) + } + } + } + } +} + +private fun Project.createTaskForComponent( + anchorTask: TaskProvider, + pub: ProjectComponentPublication, + libraryGroup: LibraryGroup?, + artifactId: Provider, + shouldPublishDocs: Provider, + isKmp: Boolean, + buildTarget: String, + kmpChildren: Set, + testModuleNames: Provider>, + isolatedProjectEnabled: Boolean, + variantName: String, +) { + val task = + createBuildInfoTask( + pub = pub, + libraryGroup = libraryGroup, + artifactId = artifactId, + shaProvider = getHeadShaProvider(), + shouldPublishDocs = shouldPublishDocs, + isKmp = isKmp, + buildTarget = buildTarget, + kmpChildren = kmpChildren, + testModuleNames = testModuleNames, + variantName = variantName, + ) + anchorTask.configure { it.dependsOn(task) } + if (!isolatedProjectEnabled) { + addTaskToAggregateBuildInfoFileTask(task) + } +} + +private fun Project.createBuildInfoTask( + pub: ProjectComponentPublication, + libraryGroup: LibraryGroup?, + artifactId: Provider, + shaProvider: Provider, + shouldPublishDocs: Provider, + isKmp: Boolean, + buildTarget: String, + kmpChildren: Set, + testModuleNames: Provider>, + variantName: String, +): TaskProvider { + val kmpTaskSuffix = computeTaskSuffix(variantName, isKmp) + + val runtimeConfigs = resolveRuntimeConfigurationNames(variantName) + + return CreateLibraryBuildInfoFileTask.setup( + project = project, + mavenGroup = libraryGroup, + variant = + VariantPublishPlan( + artifactId = artifactId, + taskSuffix = kmpTaskSuffix, + dependencies = + pub.component.map { component -> + val usageDependencies = + component.usages.orEmpty().flatMap { it.dependencies } + usageDependencies + dependenciesOnKmpVariants(component) + }, + dependencyConstraints = + pub.component.map { component -> + component.usages.orEmpty().flatMap { it.dependencyConstraints } + }, + runtimeConfigurationNames = + objects.listProperty(String::class.java).value(runtimeConfigs), + ), + shaProvider = shaProvider, + // There's a build_info file for each KMP platform, but only the artifact without a platform + // suffix is listed in docs-public/build.gradle. + shouldPublishDocs = shouldPublishDocs.map { it && kmpTaskSuffix == "" }, + isKmp = isKmp, + target = buildTarget, + kmpChildren = kmpChildren.map { modifyKmpChildrenForBuildInfo(it) }.toSet(), + testModuleNames = testModuleNames, + gradlePluginIds = + project.extensions + .findByType(GradlePluginDevelopmentExtension::class.java) + ?.plugins + ?.map { it.id } + ?.toSet() ?: emptySet(), + ) +} + +private fun Project.resolveRuntimeConfigurationNames(variantName: String): List { + val kotlinExt = kotlinExtensionOrNull + return when { + // Kotlin-only or Kotlin-enabled Android project + kotlinExt is KotlinSingleTargetExtension<*> -> { + kotlinExt.target.compilations.classpathConfigs() + } + // KMP Project + kotlinExt is KotlinMultiplatformExtension -> { + kotlinExt.targets.findByName(variantName)?.compilations?.classpathConfigs().orEmpty() + } + // Java-only Android project + extensions.findByType(AndroidComponentsExtension::class.java) != null -> { + listOf("releaseRuntimeClasspath") + } + // Standard Java or Gradle Java plugin project + plugins.hasPlugin(JavaPlugin::class.java) || + plugins.hasPlugin(JavaGradlePluginPlugin::class.java) -> { + listOf("runtimeClasspath") + } + else -> { + throw IllegalStateException( + "Project $path is not a known project type to get runtime dependencies from." + ) + } + } +} + +private fun Iterable>.classpathConfigs(): List = + asSequence() + .filterNot { it.name.contains("test", ignoreCase = true) } + .mapNotNull { it.runtimeDependencyConfigurationName } + .toList() + +private fun modifyKmpChildrenForBuildInfo(kmpChild: String): String { + // Jetbrains converts the "wasmJs" target to "wasm-js", which does not match the convention + // for other KMP targets. This is tracked in https://youtrack.jetbrains.com/issue/KT-70072 + // For now, handle this case separately. + val specialMapping = mapOf("wasmJs" to "wasm-js") + return specialMapping[kmpChild] ?: kmpChild.lowercase() +} + +private fun dependenciesOnKmpVariants(component: SoftwareComponentInternal) = + (component as? ComponentWithVariants)?.variants.orEmpty().mapNotNull { + (it as? ComponentWithCoordinates)?.coordinates?.asDependency() + } + +private fun ModuleVersionIdentifier.asDependency() = + BuildInfoVariantDependency(group, name, version) + +class BuildInfoVariantDependency(group: String, name: String, version: String) : + DefaultExternalModuleDependency(group, name, version) + +/** + * Returns the suffix which should be used for a build info file task name. + * + * For a non-KMP project, this is an empty string. + * + * For a KMP project, there is one build info task for each variant published, so to disambiguate + * the tasks each gets a suffix based on the name of the variant. For the main anchor publication + * (variant "kotlinMultiplatform") the suffix will be empty, for all other variants it will be based + * on the variant name. + * + * For examples, see CreateLibraryBuildInfoFileTaskTest + */ +@VisibleForTesting +fun computeTaskSuffix(variantName: String, isKmp: Boolean) = + if (isKmp && variantName != "kotlinMultiplatform") { + variantName.split("-").joinToString("") { word -> word.replaceFirstChar { it.uppercase() } } + } else { + "" + } + +/** + * Indicates if any of the given [PlatformIdentifier]s targets an Apple platform + * + * @param supportedPlatforms the set of [PlatformIdentifier] to examine + * @return true if any [PlatformIdentifier]s targets an Apple platform, false otherwise + */ +@VisibleForTesting +fun hasApplePlatform(supportedPlatforms: Set) = + supportedPlatforms.any { it.group == PlatformGroup.MAC } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/VariantPublishPlan.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/VariantPublishPlan.kt new file mode 100644 index 0000000000000..f4ef8bb234e05 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/VariantPublishPlan.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.buildInfo + +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.DependencyConstraint +import org.gradle.api.provider.Provider + +/** + * Info about a particular variant that will be published + * + * @param artifactId the maven artifact id + * @param taskSuffix if non-null, will be added to the end of task names to disambiguate (i.e. + * createLibraryBuildInfoFiles becomes createLibraryBuildInfoFilesJvm) + * @param dependencies provider that will return the dependencies of this variant when/if needed + */ +data class VariantPublishPlan( + val artifactId: Provider, + val taskSuffix: String = "", + val dependencies: Provider>, + val dependencyConstraints: Provider>, + val runtimeConfigurationNames: Provider>, +) diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiLocation.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiLocation.kt new file mode 100644 index 0000000000000..a36265608a89b --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiLocation.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.checkapi + +import androidx.build.Version +import androidx.build.version +import java.io.File +import java.io.Serializable +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider + +private const val BCV_DIR_NAME = "bcv" + +/** + * Contains information about the files used to record a library's API surfaces. This class may + * represent a versioned API txt file or the "current" API txt file. + * + *

+ * This class is responsible for understanding the naming pattern used by various types of API + * files: + *

    + *
  • public + *
  • restricted + *
  • resource + *
+ */ +data class ApiLocation( + // Directory where the library's API files are stored + val apiFileDirectory: File, + // File where the library's public API surface is recorded + val publicApiFile: File, + // File where the library's public plus restricted (see @RestrictTo) API surfaces are recorded + val restrictedApiFile: File, + // File where the library's public resources are recorded + val resourceFile: File, + // Directory where the library's stable AIDL surface is recorded + val aidlApiDirectory: File, + // File where the API version history is recorded, for use in docs + val apiLevelsFile: File, +) : Serializable { + + /** + * Returns the library version represented by this API location, or {@code null} if this is a + * current API file. + */ + fun version(): Version? { + val baseName = publicApiFile.nameWithoutExtension + if (baseName == CURRENT) { + return null + } + return Version(baseName) + } + + companion object { + fun fromPublicApiFile(f: File): ApiLocation { + return fromBaseName(f.parentFile, f.nameWithoutExtension) + } + + fun fromVersion(apiFileDir: File, version: Version): ApiLocation { + return fromBaseName(apiFileDir, version.toApiFileBaseName()) + } + + fun fromCurrent(apiFileDir: File): ApiLocation { + return fromBaseName(apiFileDir, CURRENT) + } + + fun isResourceApiFilename(filename: String): Boolean { + return filename.startsWith(PREFIX_RESOURCE) + } + + private fun fromBaseName(apiFileDir: File, baseName: String): ApiLocation { + return ApiLocation( + apiFileDirectory = apiFileDir, + publicApiFile = File(apiFileDir, "$baseName$EXTENSION"), + restrictedApiFile = File(apiFileDir, "$PREFIX_RESTRICTED$baseName$EXTENSION"), + resourceFile = File(apiFileDir, "$PREFIX_RESOURCE$baseName$EXTENSION"), + aidlApiDirectory = File(apiFileDir, AIDL_API_DIRECTORY_NAME).resolve(baseName), + apiLevelsFile = File(apiFileDir, API_LEVELS), + ) + } + + /** File name extension used by API files. */ + private const val EXTENSION = ".txt" + + /** Base file name used by current API files. */ + private const val CURRENT = "current" + + /** Prefix used for restricted API surface files. */ + private const val PREFIX_RESTRICTED = "restricted_" + + /** Prefix used for resource-type API files. */ + private const val PREFIX_RESOURCE = "res-" + + /** Directory name for location of AIDL API files */ + private const val AIDL_API_DIRECTORY_NAME = "aidl" + + /** File name for API version history file. */ + private const val API_LEVELS = "apiLevels.json" + } +} + +/** Converts the version to a valid API file base name. */ +private fun Version.toApiFileBaseName(): String { + return getApiFileVersion(this).toString() +} + +/** Returns the directory containing the project's versioned and current ABI files. */ +fun Project.getBcvFileDirectory(): Directory = project.layout.projectDirectory.dir(BCV_DIR_NAME) + +/** Returns the directory containing the project's versioned and current API files. */ +fun Project.getApiFileDirectory(): File { + return File(project.projectDir, "api") +} + +/** Returns the directory containing the project's built current API file. */ +private fun Project.getBuiltApiFileDirectory(): File { + @Suppress("DEPRECATION") + return File(project.buildDir, "api") +} + +/** Returns the directory containing the project's built current ABI file. */ +fun Project.getBuiltBcvFileDirectory(): Provider = + project.layout.buildDirectory.dir(BCV_DIR_NAME) + +/** + * Returns an ApiLocation with the given version, or with the project's current version if not + * specified. This method is guaranteed to return an ApiLocation that represents a versioned API txt + * and not a current API txt. + * + * @param version the project version for which an API file should be returned + * @return an ApiLocation representing a versioned API file + */ +fun Project.getVersionedApiLocation(version: Version = project.version()): ApiLocation { + return ApiLocation.fromVersion(project.getApiFileDirectory(), version) +} + +/** + * Returns an ApiLocation for the current version. This method is guaranteed to return an + * ApiLocation that represents a current API txt and not a versioned API txt. + */ +fun Project.getCurrentApiLocation(): ApiLocation { + return ApiLocation.fromCurrent(project.getApiFileDirectory()) +} + +/** + * Returns an ApiLocation for the "work-in-progress" current version which is built from tip-of-tree + * and lives in the build output directory. + */ +fun Project.getBuiltApiLocation(): ApiLocation { + return ApiLocation.fromCurrent(project.getBuiltApiFileDirectory()) +} + +/** + * Contains information about the files used to record a library's API compatibility and lint + * violation baselines. + * + *

+ * This class is responsible for understanding the naming pattern used by various types of API + * compatibility and linting violation baseline files: + *

    + *
  • public API compatibility + *
  • restricted API compatibility + *
  • API lint + *
+ */ +data class ApiBaselinesLocation( + val ignoreFileDirectory: File, + val publicApiFile: File, + val restrictedApiFile: File, + val apiLintFile: File, +) : Serializable { + + companion object { + fun fromApiLocation(apiLocation: ApiLocation): ApiBaselinesLocation { + val ignoreFileDirectory = apiLocation.apiFileDirectory + return ApiBaselinesLocation( + ignoreFileDirectory = ignoreFileDirectory, + publicApiFile = + File( + ignoreFileDirectory, + apiLocation.publicApiFile.nameWithoutExtension + EXTENSION, + ), + restrictedApiFile = + File( + ignoreFileDirectory, + apiLocation.restrictedApiFile.nameWithoutExtension + EXTENSION, + ), + apiLintFile = File(ignoreFileDirectory, "api_lint$EXTENSION"), + ) + } + + private const val EXTENSION = ".ignore" + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt new file mode 100644 index 0000000000000..cc30c98d1609b --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/ApiTasks.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.checkapi + +import androidx.build.AndroidXExtension +import androidx.build.ProjectLayoutType.Companion.isJetBrainsFork +import androidx.build.Release +import androidx.build.RunApiTasks +import androidx.build.binarycompatibilityvalidator.BinaryCompatibilityValidation +import androidx.build.getSupportRootFolder +import androidx.build.hasAndroidMultiplatformPlugin +import androidx.build.isWriteVersionedApiFilesEnabled +import androidx.build.metalava.MetalavaTasks +import androidx.build.multiplatformExtension +import androidx.build.resources.ResourceTasks +import androidx.build.stableaidl.setupWithStableAidlPlugin +import androidx.build.version +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.attributes.BuildTypeAttr +import com.android.build.api.variant.KotlinMultiplatformAndroidVariant +import com.android.build.api.variant.LibraryVariant +import java.io.File +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.type.ArtifactTypeDefinition +import org.gradle.api.attributes.Attribute +import org.gradle.api.attributes.Usage +import org.gradle.api.attributes.java.TargetJvmEnvironment +import org.gradle.api.file.RegularFile +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.Provider +import org.gradle.kotlin.dsl.getByType + +sealed class ApiTaskConfig + +data class LibraryApiTaskConfig(val variant: LibraryVariant) : ApiTaskConfig() + +object JavaApiTaskConfig : ApiTaskConfig() + +object KmpApiTaskConfig : ApiTaskConfig() + +data class AndroidMultiplatformApiTaskConfig(val variant: KotlinMultiplatformAndroidVariant) : + ApiTaskConfig() + +fun AndroidXExtension.shouldConfigureApiTasks(): Provider { + return type.map { it.checkApi is RunApiTasks.Yes } +} + +/** + * Returns whether the project should write versioned API files, e.g. `1.1.0-alpha01.txt`. + * + *

+ * When set to `true`, the `updateApi` task will write the current API surface to both `current.txt` + * and `.txt`. When set to `false`, only `current.txt` will be written. The default value + * is `true`. + */ +internal fun Project.shouldWriteVersionedApiFile(): Boolean { + // Is versioned file writing disabled globally, ex. we're on a downstream branch? + if (!project.isWriteVersionedApiFilesEnabled()) { + return false + } + + // Policy: Don't write versioned files for non-final API surfaces, ex. dev or alpha, or for + // versions that should only exist in dead-end release branches, ex. rc02+ or stable. + if ( + !project.version().isFinalApi() || + (project.version().isRC() && + project.version().preReleaseIteration?.let { it > 1 } == true) || + project.version().isStable() + ) { + return false + } + + return true +} + +fun Project.configureProjectForApiTasks(config: ApiTaskConfig, extension: AndroidXExtension) { + if (isJetBrainsFork(project)) return + // afterEvaluate required to read extension properties + afterEvaluate { + if (!extension.shouldConfigureApiTasks().get()) { + return@afterEvaluate + } + + val builtApiLocation = project.getBuiltApiLocation() + val versionedApiLocation = project.getVersionedApiLocation() + val currentApiLocation = project.getCurrentApiLocation() + val outputApiLocations = + if (project.shouldWriteVersionedApiFile()) { + listOf(versionedApiLocation, currentApiLocation) + } else { + listOf(currentApiLocation) + } + + val (compilationInputs, androidManifest) = + configureCompilationInputsAndManifest(config) ?: return@afterEvaluate + val baselinesApiLocation = ApiBaselinesLocation.fromApiLocation(currentApiLocation) + val generateApiDependencies = createReleaseApiConfiguration() + + MetalavaTasks.setupProject( + project, + compilationInputs, + generateApiDependencies, + extension, + androidManifest, + baselinesApiLocation, + builtApiLocation, + outputApiLocations, + ) + + project.setupWithStableAidlPlugin() + + if (config is LibraryApiTaskConfig) { + ResourceTasks.setupProject( + project, + config.variant.artifacts.get(SingleArtifact.PUBLIC_ANDROID_RESOURCES_LIST), + builtApiLocation, + outputApiLocations, + ) + } else if (config is AndroidMultiplatformApiTaskConfig) { + // If AGP KMP project does not enable resources, generate a blank "api" file to make + // sure the check task breaks if there were tracked resources before + ResourceTasks.setupProject( + project, + config.variant.artifacts.get(SingleArtifact.PUBLIC_ANDROID_RESOURCES_LIST).orElse { + File(project.getSupportRootFolder(), "buildSrc/blank-res-api/public.txt") + }, + builtApiLocation, + outputApiLocations, + ) + } + multiplatformExtension?.let { multiplatformExtension -> + BinaryCompatibilityValidation(project, multiplatformExtension) + .setupBinaryCompatibilityValidatorTasks() + } + } +} + +internal fun Project.configureCompilationInputsAndManifest( + config: ApiTaskConfig +): Pair?>? { + return when (config) { + is LibraryApiTaskConfig -> { + if (config.variant.name != Release.DEFAULT_PUBLISH_CONFIG) { + return null + } + CompilationInputs.fromLibraryVariant(config.variant, project) to + config.variant.artifacts.get(SingleArtifact.MERGED_MANIFEST) + } + is AndroidMultiplatformApiTaskConfig -> { + CompilationInputs.fromKmpAndroidTarget(project) to + config.variant.artifacts.get(SingleArtifact.MERGED_MANIFEST) + } + is KmpApiTaskConfig -> { + CompilationInputs.fromKmpJvmTarget(project) to null + } + is JavaApiTaskConfig -> { + val javaExtension = extensions.getByType() + val mainSourceSet = javaExtension.sourceSets.getByName("main") + CompilationInputs.fromSourceSet(mainSourceSet, this) to null + } + } +} + +internal fun Project.createReleaseApiConfiguration(): Configuration { + return configurations.findByName("ReleaseApiDependencies") + ?: configurations + .create("ReleaseApiDependencies") { + it.isCanBeConsumed = false + it.isTransitive = false + it.attributes.attribute( + BuildTypeAttr.ATTRIBUTE, + project.objects.named(BuildTypeAttr::class.java, "release"), + ) + it.attributes.attribute( + Usage.USAGE_ATTRIBUTE, + objects.named(Usage::class.java, Usage.JAVA_API), + ) + it.attributes.attribute( + ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, + ArtifactTypeDefinition.JAR_TYPE, + ) + // If this is a KMP project targeting android, make sure to select the android + // compilation and not a different jvm target compilation + if (project.hasAndroidMultiplatformPlugin()) { + it.attributes.attribute( + Attribute.of( + "org.gradle.jvm.environment", + TargetJvmEnvironment::class.java, + ), + objects.named( + TargetJvmEnvironment::class.java, + TargetJvmEnvironment.ANDROID, + ), + ) + } + } + .apply { project.dependencies.add(name, project.project(path)) } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CheckApi.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CheckApi.kt new file mode 100644 index 0000000000000..a5dcb6ea42d27 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CheckApi.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.checkapi + +import androidx.build.Version +import androidx.build.checkapi.ApiLocation.Companion.isResourceApiFilename +import androidx.build.isWriteVersionedApiFilesEnabled +import androidx.build.version +import java.io.File +import java.nio.file.Files +import kotlin.io.path.name +import org.gradle.api.GradleException +import org.gradle.api.Project + +enum class ApiType { + CLASSAPI, + RESOURCEAPI, +} + +/** + * Returns the API file containing the public API that this library promises to support This is API + * file that checkApiRelease validates against + * + * @return the API file + */ +fun Project.getRequiredCompatibilityApiFile(): File? { + return getRequiredCompatibilityApiFileFromDir( + project.getApiFileDirectory(), + project.version(), + ApiType.CLASSAPI, + enforceVersionContinuity = isWriteVersionedApiFilesEnabled(), + ) +} + +/* + * Same as getRequiredCompatibilityApiFile but also contains a restricted API file + */ +fun Project.getRequiredCompatibilityApiLocation(): ApiLocation? { + val publicFile = project.getRequiredCompatibilityApiFile() ?: return null + return ApiLocation.fromPublicApiFile(publicFile) +} + +/** + * Sometimes the version of an API file might be not equal to the version of its artifact. This is + * because under certain circumstances, APIs are not allowed to change, and in those cases we may + * stop versioning the API. This functions returns the version of API file to use given the version + * of an artifact + */ +fun getApiFileVersion(version: Version): Version { + if (!isValidArtifactVersion(version)) { + val suggestedVersion = Version("${version.major}.${version.minor}.${version.patch}-rc01") + throw GradleException( + "Illegal version $version . It is not allowed to have a nonzero " + + "patch number and be alpha or beta at the same time.\n" + + "Did you mean $suggestedVersion?" + ) + } + return Version( + major = version.major, + minor = version.minor, + patch = 0, + preRelease = if (version.patch != 0) null else version.preRelease, + buildMetadata = null, + ) +} + +/** Whether it is allowed for an artifact to have this version */ +fun isValidArtifactVersion(version: Version): Boolean { + return !(version.patch != 0 && (version.isAlpha() || version.isBeta() || version.isDev())) +} + +/** + * Returns the api file that version is required to be compatible with. If apiType is + * RESOURCEAPI, it will return the resource api file and if it is CLASSAPI, it will return the + * regular api file. + */ +fun getRequiredCompatibilityApiFileFromDir( + apiDir: File, + apiVersion: Version, + apiType: ApiType, + enforceVersionContinuity: Boolean = true, +): File? { + if (!apiDir.exists()) { + return null + } + + val stream = Files.newDirectoryStream(apiDir.toPath()) + val versions = + stream.mapNotNull { path -> + val pathName = path.name + if ( + (apiType == ApiType.RESOURCEAPI && isResourceApiFilename(pathName)) || + (apiType == ApiType.CLASSAPI && !isResourceApiFilename(pathName)) + ) { + val pathVersion = Version.parseFilenameOrNull(pathName) + if (pathVersion == null) return@mapNotNull null + return@mapNotNull pathVersion to path + } + return@mapNotNull null + } + stream.close() + + val sortedVersions = versions.sortedBy { it.first } + + if (enforceVersionContinuity) { + // Validate that we are not skipping major or minor versions. + sortedVersions.zipWithNext().forEach { (older, newer) -> + val olderVersion = older.first + val newerVersion = newer.first + check(olderVersion.major + 1 >= newerVersion.major) { + "Unexpected jump in version from $olderVersion to $newerVersion" + } + check(olderVersion.minor + 1 >= newerVersion.minor) { + "Unexpected jump in version from $olderVersion to $newerVersion" + } + } + sortedVersions.lastOrNull()?.let { (version, _) -> + check(version.major + 1 >= apiVersion.major) { + "Unexpected jump in version from $version to current version $apiVersion" + } + check(version.minor + 1 >= apiVersion.minor) { + "Unexpected jump in version from $version to current version $apiVersion" + } + } + } + + // Find the path with highest version that is the same major version as the current API version. + return sortedVersions + .lastOrNull { it.first.major == apiVersion.major && it.first <= apiVersion } + ?.second + ?.toFile() +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt new file mode 100644 index 0000000000000..a34f72dfcfd95 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/checkapi/CompilationInputs.kt @@ -0,0 +1,328 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.checkapi + +import androidx.build.getAndroidJar +import androidx.build.multiplatformExtension +import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget +import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.api.variant.LibraryVariant +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.SourceSet +import org.gradle.kotlin.dsl.listProperty +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.utils.addToStdlib.foldMap + +/** + * [CompilationInputs] contains the information required to compile Java/Kotlin code. This can be + * helpful for creating Metalava and Kzip tasks with the same settings. + * + * There are two implementations: [StandardCompilationInputs] for non-multiplatform projects and + * [MultiplatformCompilationInputs] for multiplatform projects. + */ +internal sealed interface CompilationInputs { + /** Source files to process */ + val sourcePaths: FileCollection + + /** Dependencies (compiled classes) of [sourcePaths]. */ + val dependencyClasspath: FileCollection + + /** Android's boot classpath. */ + val bootClasspath: FileCollection + + companion object { + /** Constructs a [CompilationInputs] from a library and its variant */ + fun fromLibraryVariant(variant: LibraryVariant, project: Project): CompilationInputs { + // The boot classpath is common to both multiplatform and standard configurations. + val bootClasspath = + project.files( + project.extensions + .findByType(LibraryAndroidComponentsExtension::class.java)!! + .sdkComponents + .bootClasspath + ) + + // Not a multiplatform project, set up standard inputs + val kotlinCollection = project.files(variant.sources.kotlin?.all) + val javaCollection = project.files(variant.sources.java?.all) + val sourceCollection = kotlinCollection + javaCollection + + @Suppress("UnstableApiUsage") // Usage of compileClasspath + return StandardCompilationInputs( + sourcePaths = sourceCollection, + dependencyClasspath = variant.compileClasspath, + bootClasspath = bootClasspath, + ) + } + + /** + * Returns the CompilationInputs for the `jvm` target of a KMP project. + * + * @param project The project whose main jvm target inputs will be returned. + */ + fun fromKmpJvmTarget(project: Project): CompilationInputs { + val kmpExtension = + checkNotNull(project.multiplatformExtension) { + """ + ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its + jvm source sets. + """ + .trimIndent() + } + val jvmTarget = kmpExtension.targets.requirePlatform(KotlinPlatformType.jvm) + val jvmCompilation = + jvmTarget.findCompilation(compilationName = KotlinCompilation.MAIN_COMPILATION_NAME) + + return MultiplatformCompilationInputs.fromCompilation( + project = project, + kmpExtension = kmpExtension, + mainCompilationProvider = jvmCompilation, + bootClasspath = project.getAndroidJar(), + ) + } + + /** + * Returns the CompilationInputs for the `android` target of a KMP project. + * + * @param project The project whose main android target inputs will be returned. + */ + fun fromKmpAndroidTarget(project: Project): CompilationInputs { + val kmpExtension = + checkNotNull(project.multiplatformExtension) { + """ + ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its + android source sets. + """ + .trimIndent() + } + val target = + kmpExtension.targets + .withType(KotlinMultiplatformAndroidLibraryTarget::class.java) + .single() + val compilation = target.findCompilation(KotlinCompilation.MAIN_COMPILATION_NAME) + + val bootClasspath = + project.files( + project.extensions + .findByType(KotlinMultiplatformAndroidComponentsExtension::class.java)!! + .sdkComponents + .bootClasspath + ) + return MultiplatformCompilationInputs.fromCompilation( + project = project, + kmpExtension = kmpExtension, + mainCompilationProvider = compilation, + bootClasspath = bootClasspath, + ) + } + + /** Constructs a [CompilationInputs] from a sourceset */ + fun fromSourceSet(sourceSet: SourceSet, project: Project): CompilationInputs { + val sourcePaths: FileCollection = + project.files(project.provider { sourceSet.allSource.srcDirs }) + val dependencyClasspath = sourceSet.compileClasspath + return StandardCompilationInputs( + sourcePaths = sourcePaths, + dependencyClasspath = dependencyClasspath, + bootClasspath = project.getAndroidJar(), + ) + } + + /** + * Returns the list of Files (might be directories) that are included in the compilation of + * this target. + * + * @param compilationName The name of the compilation. A target might have separate + * compilations (e.g. main vs test for jvm or debug vs release for Android) + */ + private fun KotlinTarget.findCompilation( + compilationName: String + ): Provider> { + return project.provider { + val selectedCompilation = + checkNotNull(compilations.findByName(compilationName)) { + """ + Cannot find $compilationName compilation configuration of $name in + ${project.path}. + Available compilations: ${compilations.joinToString(", ") { it.name }} + """ + .trimIndent() + } + selectedCompilation + } + } + + /** + * Returns the [KotlinTarget] that targets the given platform type. + * + * This method will throw if there are no matching targets or there are more than 1 matching + * target. + */ + private fun Collection.requirePlatform( + expectedPlatformType: KotlinPlatformType + ): KotlinTarget { + return this.singleOrNull { it.platformType == expectedPlatformType } + ?: error( + """ + Expected 1 and only 1 kotlin target with $expectedPlatformType. Found $size. + Matching compilation targets: + ${joinToString(",") { it.name }} + All compilation targets: + ${this@requirePlatform.joinToString(",") { it.name }} + """ + .trimIndent() + ) + } + } +} + +/** Compile inputs for a regular (non-multiplatform) project */ +internal data class StandardCompilationInputs( + override val sourcePaths: FileCollection, + override val dependencyClasspath: FileCollection, + override val bootClasspath: FileCollection, +) : CompilationInputs + +/** Compile inputs for a single source set from a multiplatform project. */ +internal data class SourceSetInputs( + /** Name of the source set, e.g. "androidMain" */ + val sourceSetName: String, + /** Names of other source sets that this one depends on */ + val dependsOnSourceSets: List, + /** Source files of this source set */ + val sourcePaths: FileCollection, + /** Compile dependencies for this source set */ + val dependencyClasspath: FileCollection, + /** The platforms which this source set can be a part of a compilation for. */ + val kotlinPlatforms: Set, +) + +/** Inputs for a single compilation of a multiplatform project (just the android or jvm target) */ +internal class MultiplatformCompilationInputs( + project: Project, + /** + * The [SourceSetInputs] for this project's source sets. This is a [Provider] because not all + * relationships between source sets will be loaded at configuration time. + */ + val sourceSets: Provider>, + // Classpath for the android or jvm compilation. + override val dependencyClasspath: FileCollection, + override val bootClasspath: FileCollection, + // Source paths for all files involved in the android or jvm compilation. + override val sourcePaths: ConfigurableFileCollection, +) : CompilationInputs { + /** + * Dependencies aggregated from all compilations (the [dependencyClasspath] only includes the + * main jvm or android compilation). + */ + val allSourceSetsDependencyClasspath = + project.files(sourceSets.map { it.map { sourceSet -> sourceSet.dependencyClasspath } }) + + /** Source files from the KMP common module of this project */ + val commonModuleSourcePaths: FileCollection = + project.files( + sourceSets.map { + it.filter { sourceSet -> sourceSet.dependsOnSourceSets.isEmpty() } + .map { sourceSet -> sourceSet.sourcePaths } + } + ) + + companion object { + /** + * Creates inputs based on a multiplatform project. + * + * The [mainCompilationProvider] is used for the + * [MultiplatformCompilationInputs.dependencyClasspath] and + * [MultiplatformCompilationInputs.sourcePaths], but all compilations from the + * [kmpExtension] are included in the [MultiplatformCompilationInputs.sourceSets]. + */ + fun fromCompilation( + project: Project, + kmpExtension: KotlinMultiplatformExtension, + mainCompilationProvider: Provider>, + bootClasspath: FileCollection, + ): MultiplatformCompilationInputs { + // Find the sources and dependencies just from the main compilation. + val compileDependencies = mainCompilationProvider.map { it.compileDependencyFiles } + val sourcePaths = + project.files( + mainCompilationProvider.map { compilation -> + compilation.allKotlinSourceSets.map { sourceSet -> + sourceSet.kotlin.sourceDirectories + } + } + ) + + // List all main compilations. + val allCompilations = project.objects.listProperty>() + kmpExtension.targets.configureEach { target -> + val mainCompilation = + target.compilations.named(KotlinCompilation.MAIN_COMPILATION_NAME) + allCompilations.add(mainCompilation) + } + + // Only include main source sets, not test. + val allKotlinSourceSets = project.objects.listProperty() + kmpExtension.sourceSets.configureEach { + if (it.name.lowercase().contains("test")) return@configureEach + allKotlinSourceSets.add(it) + } + + val sourceSets = + allKotlinSourceSets.zip(allCompilations) { sourceSets, allCompilations -> + sourceSets.map { sourceSet -> + // Find the compilations that this source set is part of. + val allAssociatedCompilations = + allCompilations.filter { it.allKotlinSourceSets.contains(sourceSet) } + allAssociatedCompilations.map { it.compileDependencyFiles } + // Include dependencies from all compilations which this source set is + // associated with. + val sourceSetDependencies = + allAssociatedCompilations.foldMap( + { it.compileDependencyFiles }, + { fc1, fc2 -> fc1 + fc2 }, + ) + val kotlinPlatforms = + allAssociatedCompilations.map { it.platformType }.toSet() + SourceSetInputs( + sourceSet.name, + sourceSet.dependsOn.map { it.name }, + sourceSet.kotlin.sourceDirectories, + sourceSetDependencies, + kotlinPlatforms, + ) + } + } + + return MultiplatformCompilationInputs( + project, + sourceSets, + project.files(compileDependencies), + bootClasspath, + sourcePaths, + ) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/AndroidXClang.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/AndroidXClang.kt new file mode 100644 index 0000000000000..aaa57fe4b0c23 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/AndroidXClang.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import org.gradle.api.Action +import org.gradle.api.Project +import org.jetbrains.kotlin.konan.target.LinkerOutputKind + +/** Not internal to be able to use in buildSrc-tests */ +class AndroidXClang(val project: Project) { + private val multiTargetNativeCompilations = mutableMapOf() + + fun createNativeCompilation( + archiveName: String, + outputKind: LinkerOutputKind, + configure: Action, + ): MultiTargetNativeCompilation { + val multiTargetNativeCompilation = + multiTargetNativeCompilations.getOrPut(archiveName) { + MultiTargetNativeCompilation( + project = project, + archiveName = archiveName, + outputKind = outputKind, + ) + } + configure.execute(multiTargetNativeCompilation) + return multiTargetNativeCompilation + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangArchiveTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangArchiveTask.kt new file mode 100644 index 0000000000000..47836cee4bf78 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangArchiveTask.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.services.ServiceReference +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor + +@CacheableTask +abstract class ClangArchiveTask @Inject constructor(private val workerExecutor: WorkerExecutor) : + DefaultTask() { + init { + description = "Combines multiple object files (.o) into an archive file (.a)." + group = "Build" + } + + @get:ServiceReference(KonanBuildService.KEY) + abstract val konanBuildService: Property + + @get:Nested abstract val llvmArchiveParameters: ClangArchiveParameters + + @TaskAction + fun archive() { + workerExecutor.noIsolation().submit(ClangArchiveWorker::class.java) { + it.llvmArchiveParameters.set(llvmArchiveParameters) + it.buildService.set(konanBuildService) + } + } +} + +abstract class ClangArchiveParameters { + /** The target platform for the archive file. */ + @get:Input abstract val konanTarget: Property + + /** The list of object files that needs to be added to the archive. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.NAME_ONLY) + abstract val objectFiles: ConfigurableFileCollection + + /** The final output file that will include the archive of the given [objectFiles]. */ + @get:OutputFile abstract val outputFile: RegularFileProperty +} + +private abstract class ClangArchiveWorker : WorkAction { + interface Params : WorkParameters { + val llvmArchiveParameters: Property + val buildService: Property + } + + override fun execute() { + val buildService = parameters.buildService.get() + buildService.archiveLibrary(parameters.llvmArchiveParameters.get()) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangCompileTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangCompileTask.kt new file mode 100644 index 0000000000000..04cbc61ad208c --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangCompileTask.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.services.ServiceReference +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor + +@CacheableTask +abstract class ClangCompileTask @Inject constructor(private val workerExecutor: WorkerExecutor) : + DefaultTask() { + init { + description = "Compiles C sources into an object file (.o)." + group = "Build" + } + + @get:ServiceReference(KonanBuildService.KEY) + abstract val konanBuildService: Property + + @get:Nested abstract val clangParameters: ClangCompileParameters + + @TaskAction + fun compile() { + workerExecutor.noIsolation().submit(ClangCompileWorker::class.java) { + it.buildService.set(konanBuildService) + it.clangParameters.set(clangParameters) + } + } +} + +abstract class ClangCompileParameters { + /** The compilation target platform for which the given inputs will be compiled. */ + @get:Input abstract val konanTarget: Property + + /** List of C source files. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.NAME_ONLY) + abstract val sources: ConfigurableFileCollection + + /** The output directory where the object files for each source file will be written. */ + @get:OutputDirectory abstract val output: DirectoryProperty + + /** List of directories that include the headers used in the compilation. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val includes: ConfigurableFileCollection + + /** List of arguments that will be passed into clang during compilation. */ + @get:Input abstract val freeArgs: ListProperty +} + +private abstract class ClangCompileWorker : WorkAction { + interface Params : WorkParameters { + val clangParameters: Property + val buildService: Property + } + + override fun execute() { + val buildService = parameters.buildService.get() + buildService.compile(parameters.clangParameters.get()) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangLinkerTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangLinkerTask.kt new file mode 100644 index 0000000000000..57033637280ea --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/ClangLinkerTask.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.services.ServiceReference +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.konan.target.LinkerOutputKind + +@CacheableTask +abstract class ClangLinkerTask @Inject constructor(private val workerExecutor: WorkerExecutor) : + DefaultTask() { + init { + description = + "Combines multiple object files (.o) into either a shared library file" + + "(.so / .dylib) or an executable." + group = "Build" + } + + @get:ServiceReference(KonanBuildService.KEY) + abstract val konanBuildService: Property + + @get:Nested abstract val clangParameters: ClangLinkerParameters + + @TaskAction + fun archive() { + workerExecutor.noIsolation().submit(ClangLinkerWorker::class.java) { + it.clangParameters.set(clangParameters) + it.buildService.set(konanBuildService) + } + } +} + +abstract class ClangLinkerParameters { + + /** The kind of output the linker should produce. */ + @get:Input abstract val linkerOutputKind: Property + + /** The target platform for the shared file. */ + @get:Input abstract val konanTarget: Property + + /** List of object files that will be added to the shared file output. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.NAME_ONLY) + abstract val objectFiles: ConfigurableFileCollection + + /** + * The final output file that will include the shared library containing the given + * [objectFiles]. + */ + @get:OutputFile abstract val outputFile: RegularFileProperty + + /** + * List of additional objects that will be dynamically linked with the output file. At runtime, + * these need to be available for the shared object output to work. + */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.NAME_ONLY) + abstract val linkedObjects: ConfigurableFileCollection + + /** List of arguments that will be passed into linker when creating a shared library. */ + @get:Input abstract val linkerArgs: ListProperty +} + +private abstract class ClangLinkerWorker : WorkAction { + interface Params : WorkParameters { + val clangParameters: Property + val buildService: Property + } + + override fun execute() { + val buildService = parameters.buildService.get() + buildService.runLinker(parameters.clangParameters.get()) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/CombineObjectFilesTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/CombineObjectFilesTask.kt new file mode 100644 index 0000000000000..e61229bdce622 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/CombineObjectFilesTask.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.konan.target.Architecture +import org.jetbrains.kotlin.konan.target.Family +import org.jetbrains.kotlin.konan.target.KonanTarget + +/** + * Combines all given [objectFiles] into a directory with a well defined directory structure. + * + * The Android targets will be placed into a directory structure that matches the jniLibs structure + * of Android Gradle Plugin, e.g.: + * ``` + * + * armeabi-v7a/libfoo.so + * arm64-v8a/libfoo.so + * x86/libfoo.so + * x86_64/libfoo.so + * ``` + * + * Desktop targets will be placed on a structure that is based on the OS and architecture. e.g.: + * ``` + * + * linux_arm64/libfoo.so + * linux_x64/libfoo.so + * osx_arm64/libfoo.dylib + * osx_x64/libfoo.dylib + * windows_x64/foo.dll + * ``` + */ +@DisableCachingByDefault(because = "not worth caching,just copies inputs into a another directory") +abstract class CombineObjectFilesTask : DefaultTask() { + @get:Nested abstract val objectFiles: ListProperty> + + @get:OutputDirectory abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun combineLibraries() { + // TODO: (b/304281116) figure out how we'll have a single source of truth between the logic + // here and the runtime logic. + val outputDir = outputDirectory.get().asFile + outputDir.deleteRecursively() + outputDir.mkdirs() + val resolvedObjectFiles = objectFiles.get().map { it.get() } + check(resolvedObjectFiles.isNotEmpty()) { + "Running CombineSharedLibrariesTask without any inputs, this is likely an error" + } + resolvedObjectFiles.forEach { objectFile -> + val konanTarget = objectFile.konanTarget.get().asKonanTarget + val targetFile = targetFileFor(outputDir, konanTarget, objectFile) + targetFile.parentFile?.mkdirs() + objectFile.file.get().asFile.copyTo(target = targetFile, overwrite = true) + } + } + + companion object { + private val familyDirectoryPrefixes = + mapOf(Family.LINUX to "linux", Family.MINGW to "windows", Family.OSX to "osx") + + private val architectureSuffixes = + mapOf( + Architecture.ARM32 to "arm32", + Architecture.ARM64 to "arm64", + Architecture.X64 to "x64", + Architecture.X86 to "x86", + ) + + private fun targetFileFor( + outputDir: File, + konanTarget: KonanTarget, + objectFile: ObjectFile, + ) = outputDir.resolve(directoryName(konanTarget)).resolve(objectFile.file.get().asFile.name) + + private fun directoryName(konanTarget: KonanTarget): String { + if (konanTarget.family == Family.ANDROID) { + // use android's own native library directory convention + // https://developer.android.com/ndk/guides/abis#sa + return when (konanTarget.architecture) { + Architecture.X86 -> "x86" + Architecture.X64 -> "x86_64" + Architecture.ARM32 -> "armeabi-v7a" + Architecture.ARM64 -> "arm64-v8a" + } + } + val familyPrefix = + familyDirectoryPrefixes[konanTarget.family] + ?: error("Unsupported family ${konanTarget.family} for $konanTarget") + val architectureSuffix = + architectureSuffixes[konanTarget.architecture] + ?: error( + "Unsupported architecture ${konanTarget.architecture} for $konanTarget" + ) + return "natives/${familyPrefix}_$architectureSuffix" + } + } +} + +/** + * Configures the [CombineObjectFilesTask] with the outputs of the [multiTargetNativeCompilation] + * based on the given target [filter]. + */ +fun TaskProvider.configureFrom( + multiTargetNativeCompilation: MultiTargetNativeCompilation, + filter: (KonanTarget) -> Boolean, +) { + configure { task -> + task.objectFiles.addAll( + multiTargetNativeCompilation.targetsProvider(filter).map { nativeTargetCompilations -> + nativeTargetCompilations.map { nativeTargetCompilation -> + nativeTargetCompilation.linkerTask.map { linkerTask -> + ObjectFile( + konanTarget = linkerTask.clangParameters.konanTarget, + file = linkerTask.clangParameters.outputFile, + ) + } + } + } + ) + } +} + +/** Represents an object file (.o, .so) associated with its [konanTarget]. */ +class ObjectFile( + @get:Input val konanTarget: Provider, + @get:InputFile @get:PathSensitive(PathSensitivity.NAME_ONLY) val file: RegularFileProperty, +) diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/CreateDefFileWithLibraryPathTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/CreateDefFileWithLibraryPathTask.kt new file mode 100644 index 0000000000000..6fbce251642d7 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/CreateDefFileWithLibraryPathTask.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** + * Creates a CInterop def file based on an [original] with added static library path to include the + * given [objectFile]. + * + * Once KT-62800 is fixed, we can consider removing this task and do all of this programmatically. + * + * https://kotlinlang.org/docs/native-c-interop.html + */ +@DisableCachingByDefault(because = "not worth caching, it is a copy with file modification") +abstract class CreateDefFileWithLibraryPathTask : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val original: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.NAME_ONLY) + abstract val objectFile: RegularFileProperty + + @get:OutputFile abstract val target: RegularFileProperty + + @get:Internal abstract val projectDir: DirectoryProperty + + @TaskAction + fun createPlatformSpecificDefFile() { + val target = target.asFile.get() + target.parentFile?.mkdirs() + // use relative path to the owning project so it can be cached (as much as possible). + // Right now,the only way to add libraryPaths/staticLibraries is the def file, which + // resolves paths relative to the project. Once KT-62800 is fixed, we should remove this + // task but until than, this is the only option to pass a generated so file + val objectFileParentDir = + objectFile.asFile + .get() + .parentFile + .canonicalFile + .relativeTo(projectDir.get().asFile.canonicalFile) + val outputContents = + listOf( + original.asFile.get().readText(Charsets.UTF_8), + "libraryPaths=\"$objectFileParentDir\"", + "staticLibraries=" + objectFile.asFile.get().name, + ) + .joinToString(System.lineSeparator()) + target.writeText(outputContents, Charsets.UTF_8) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt new file mode 100644 index 0000000000000..85fde3729f738 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanBuildService.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import androidx.build.KonanPrebuiltsSetup +import androidx.build.ProjectLayoutType +import androidx.build.clang.KonanBuildService.Companion.obtain +import androidx.build.getKonanPrebuiltsFolder +import java.io.ByteArrayOutputStream +import javax.inject.Inject +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import org.gradle.api.tasks.Optional +import org.gradle.process.ExecOperations +import org.gradle.process.ExecSpec +import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper +import org.jetbrains.kotlin.gradle.utils.NativeCompilerDownloader +import org.jetbrains.kotlin.konan.TempFiles +import org.jetbrains.kotlin.konan.target.Family +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.jetbrains.kotlin.konan.target.LinkerArguments +import org.jetbrains.kotlin.konan.target.Platform +import org.jetbrains.kotlin.konan.target.PlatformManager + +/** + * A Gradle BuildService that provides access to Konan Compiler (clang, linker, ar etc) to build + * native sources for multiple targets. + * + * You can obtain the instance via [obtain]. + * + * @see ClangArchiveTask + * @see ClangCompileTask + * @see ClangLinkerTask + */ +abstract class KonanBuildService @Inject constructor(private val execOperations: ExecOperations) : + BuildService { + private val dist by lazy { + KonanPrebuiltsSetup.createKonanDistribution( + prebuiltsDirectory = parameters.prebuilts.orNull?.asFile, + konanHome = parameters.konanHome.get().asFile, + ) + } + + private val platformManager by lazy { PlatformManager(distribution = dist) } + + /** @see ClangCompileTask */ + fun compile(parameters: ClangCompileParameters) { + val outputDir = parameters.output.get().asFile + outputDir.deleteRecursively() + outputDir.mkdirs() + + val platform = getPlatform(parameters.konanTarget) + val additionalArgs = buildList { + addAll(parameters.freeArgs.get()) + add("--compile") + parameters.includes.files.forEach { includeDirectory -> + check(includeDirectory.isDirectory) { + "Include parameter for clang must be a directory: $includeDirectory" + } + add("-I${includeDirectory.canonicalPath}") + } + addAll(parameters.sources.regularFilePaths()) + } + + val clangCommand = platform.clang.clangC(*additionalArgs.toTypedArray()) + execOperations.executeSilently { execSpec -> + execSpec.executable = clangCommand.first() + execSpec.args(clangCommand.drop(1)) + execSpec.workingDir = parameters.output.get().asFile + } + } + + /** @see ClangArchiveTask */ + fun archiveLibrary(parameters: ClangArchiveParameters) { + val outputFile = parameters.outputFile.get().asFile + outputFile.delete() + outputFile.parentFile.mkdirs() + + val platform = getPlatform(parameters.konanTarget) + val llvmArgs = buildList { + add("rc") + add(parameters.outputFile.get().asFile.canonicalPath) + addAll(parameters.objectFiles.regularFilePaths()) + } + val commands = platform.clang.llvmAr(*llvmArgs.toTypedArray()) + execOperations.executeSilently { execSpec -> + execSpec.executable = commands.first() + execSpec.args(commands.drop(1)) + } + } + + /** @see ClangLinkerTask */ + fun runLinker(parameters: ClangLinkerParameters) { + val outputFile = parameters.outputFile.get().asFile + outputFile.delete() + outputFile.parentFile.mkdirs() + + val platform = getPlatform(parameters.konanTarget) + + // Specify max-page-size to align ELF regions to 16kb and use LLVM linker + // See https://youtrack.jetbrains.com/issue/KT-71728 + val linkerFlags = + parameters.linkerArgs.get() + + if (parameters.konanTarget.get().asKonanTarget.family == Family.ANDROID) { + listOf("-fuse-ld=lld", "-z", "max-page-size=16384") + } else { + emptyList() + } + + val objectFiles = parameters.objectFiles.regularFilePaths() + val linkedObjectFiles = parameters.linkedObjects.regularFilePaths() + val linkCommands = + with(platform.linker) { + LinkerArguments( + TempFiles(), + objectFiles = objectFiles, + executable = outputFile.canonicalPath, + dynamicLibraries = linkedObjectFiles, + staticLibraries = emptyList(), + linkerArgs = linkerFlags, + optimize = true, + debug = false, + kind = parameters.linkerOutputKind.get(), + outputDsymBundle = "unused", + sanitizer = null, + ) + .finalLinkCommands() + } + linkCommands + .map { it.argsWithExecutable } + .forEach { args -> + execOperations.executeSilently { execSpec -> + execSpec.executable = args.first() + args + .drop(1) + .filter(getLinkerArgsFilter(parameters.konanTarget.get().asKonanTarget)) + .forEach { execSpec.args(it) } + } + } + } + + private fun getLinkerArgsFilter(target: KonanTarget): (String) -> Boolean = { flag -> + // We use the linker that konan uses to be as similar as possible but that linker also has + // extra things we might not want or need, In the future, we can consider not using the + // `platform.linker` but then we would need to parse the konan.properties file to get the + // relevant necessary parameters like sysroot, etc. + // https://github.com/JetBrains/kotlin/blob/master/kotlin-native/konan/konan.properties + when { + // Remove konan demangling, which we don't need and is not available in the default + // distribution. + flag == "--defsym" || flag.contains("Konan_cxa_demangle") -> false + // b/414635735 - Remove flag to explicitly link with the shared version of GCC runtime + // library as that is not widely available in all Linux distribution and we prefer + // linking to the static version (via -lgcc). Found in 'linkerGccFlags' in + // the konan.properties. + target.family == Family.LINUX && flag == "-lgcc_s" -> false + else -> true + } + } + + private fun FileCollection.regularFilePaths(): List { + return files + .flatMap { it.walkTopDown().filter { it.isFile }.map { it.canonicalPath } } + .distinct() + } + + private fun getPlatform(serializableKonanTarget: Property): Platform { + val konanTarget = serializableKonanTarget.get().asKonanTarget + check(platformManager.enabled.contains(konanTarget)) { + "cannot find enabled target with name ${serializableKonanTarget.get()}" + } + val platform = platformManager.platform(konanTarget) + platform.downloadDependencies() + return platform + } + + /** Execute the command without logs unless it fails. */ + private fun ExecOperations.executeSilently(block: (ExecSpec) -> T) { + val outputStream = ByteArrayOutputStream() + val errorStream = ByteArrayOutputStream() + val execResult = exec { + block(it) + it.errorOutput = errorStream + it.standardOutput = outputStream + it.isIgnoreExitValue = true // we'll check it below + } + if (execResult.exitValue != 0) { + throw GradleException( + """ + Compilation failed: + ==== output: + ${outputStream.toString(Charsets.UTF_8)} + ==== error: + ${errorStream.toString(Charsets.UTF_8)} + """ + .trimIndent() + ) + } + } + + interface Parameters : BuildServiceParameters { + /** KONAN_HOME parameter for initializing konan */ + val konanHome: DirectoryProperty + + /** Location if konan prebuilts. Can be null if this is a playground project */ + @get:Optional val prebuilts: DirectoryProperty + + /** + * The type of the project (Playground vs AOSP main). This value is used to ensure we + * initialize Konan distribution properly. + */ + val projectLayoutType: Property + } + + companion object { + internal const val KEY = "konanBuildService" + + fun obtain(project: Project): Provider { + return project.gradle.sharedServices.registerIfAbsent( + KEY, + KonanBuildService::class.java, + ) { + check(project.plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java)) { + "KonanBuildService can only be used in projects that applied the KMP plugin" + } + check(KonanPrebuiltsSetup.isConfigured(project)) { + "Konan prebuilt directories are not configured for project \"${project.path}\"" + } + val nativeCompilerDownloader = NativeCompilerDownloader(project) + nativeCompilerDownloader.downloadIfNeeded() + + it.parameters.konanHome.set(nativeCompilerDownloader.compilerDirectory) + it.parameters.projectLayoutType.set(ProjectLayoutType.from(project)) + if (!ProjectLayoutType.isPlayground(project)) { + it.parameters.prebuilts.set(project.getKonanPrebuiltsFolder()) + } + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanCinteropExt.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanCinteropExt.kt new file mode 100644 index 0000000000000..084e08c209707 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/KonanCinteropExt.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import com.android.utils.appendCapitalized +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation +import org.jetbrains.kotlin.konan.target.HostManager +import org.jetbrains.kotlin.konan.target.KonanTarget + +/** + * Configures a CInterop for the given [kotlinNativeCompilation]. The cinterop will be based on the + * [cinteropName] in the project sources but will additionally include the references to the library + * archive from the [ClangArchiveTask] so that it can be embedded in the generated klib of the + * cinterop. + */ +internal fun MultiTargetNativeCompilation.configureCinterop( + kotlinNativeCompilation: KotlinNativeCompilation, + cinteropName: String = archiveName, +) { + val kotlinNativeTarget = kotlinNativeCompilation.target + if (!canCompileOnCurrentHost(kotlinNativeTarget.konanTarget)) { + return + } + val konanTarget = kotlinNativeTarget.konanTarget + val nativeTargetCompilation = targetProvider(konanTarget) + val taskNamePrefix = "androidXCinterop".appendCapitalized(kotlinNativeTarget.name, archiveName) + val createDefFileTask = + registerCreateDefFileTask( + project = project, + taskNamePrefix = taskNamePrefix, + konanTarget = konanTarget, + archiveProvider = + nativeTargetCompilation + .flatMap { it.archiveTask } + .flatMap { it.llvmArchiveParameters.outputFile }, + cinteropName = cinteropName, + ) + registerCInterop( + kotlinNativeCompilation, + cinteropName, + createDefFileTask, + nativeTargetCompilation, + ) +} + +/** + * Configures a CInterop for the given [kotlinNativeCompilation]. The cinterop will be based on the + * [archiveConfiguration] name in the project sources but will additionally include the references + * to the library archive from the [ClangArchiveTask] so that it can be embedded in the generated + * klib of the cinterop. + */ +internal fun configureCinterop( + project: Project, + kotlinNativeCompilation: KotlinNativeCompilation, + archiveConfiguration: Configuration, +) { + val kotlinNativeTarget = kotlinNativeCompilation.target + if (!HostManager().isEnabled(kotlinNativeTarget.konanTarget)) { + return + } + val taskNamePrefix = + "androidXCinterop".appendCapitalized(kotlinNativeTarget.name, archiveConfiguration.name) + val createDefFileTask = + registerCreateDefFileTask( + project = project, + taskNamePrefix = taskNamePrefix, + konanTarget = kotlinNativeCompilation.konanTarget, + archiveProvider = + project.layout.file(archiveConfiguration.elements.map { it.single().asFile }), + cinteropName = archiveConfiguration.name, + ) + registerCInterop(kotlinNativeCompilation, archiveConfiguration.name, createDefFileTask) +} + +private fun registerCreateDefFileTask( + project: Project, + taskNamePrefix: String, + konanTarget: KonanTarget, + archiveProvider: Provider, + cinteropName: String, +) = + project.tasks.register( + taskNamePrefix.appendCapitalized("createDefFileFor", konanTarget.name), + CreateDefFileWithLibraryPathTask::class.java, + ) { task -> + task.objectFile.set(archiveProvider) + task.target.set( + project.layout.buildDirectory.file( + "cinteropDefFiles/$taskNamePrefix/${konanTarget.name}/$cinteropName.def" + ) + ) + task.original.set( + project.layout.projectDirectory.file("src/nativeInterop/cinterop/$cinteropName.def") + ) + task.projectDir.set(project.layout.projectDirectory) + } + +private fun registerCInterop( + kotlinNativeCompilation: KotlinNativeCompilation, + cinteropName: String, + createDefFileTask: TaskProvider, + nativeTargetCompilation: Provider? = null, +) { + kotlinNativeCompilation.cinterops.register(cinteropName) { cInteropSettings -> + cInteropSettings.definitionFile.set(createDefFileTask.flatMap { it.target }) + nativeTargetCompilation?.let { nativeTargetCompilation -> + cInteropSettings.includeDirs( + nativeTargetCompilation + .flatMap { it.compileTask } + .map { it.clangParameters.includes } + ) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/MultiTargetNativeCompilation.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/MultiTargetNativeCompilation.kt new file mode 100644 index 0000000000000..17d696f961587 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/MultiTargetNativeCompilation.kt @@ -0,0 +1,287 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import com.android.utils.appendCapitalized +import org.gradle.api.Action +import org.gradle.api.NamedDomainObjectFactory +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.listProperty +import org.jetbrains.kotlin.konan.target.HostManager +import org.jetbrains.kotlin.konan.target.KonanTarget +import org.jetbrains.kotlin.konan.target.LinkerOutputKind + +/** + * A native compilation setup (C code) that can target multiple platforms. + * + * New targets can be added via the [configureTarget] method. Each configured target will have tasks + * to produce machine code (.o), shared library (.so / .dylib) or archive (.a). + * + * Common configuration between targets can be done via the [configureEachTarget] method. + * + * @see NativeTargetCompilation for configuration details for each target. + */ +class MultiTargetNativeCompilation( + internal val project: Project, + internal val archiveName: String, + internal val outputKind: LinkerOutputKind, +) { + private val hostManager = HostManager() + + private val nativeTargets = + project.objects.domainObjectContainer( + NativeTargetCompilation::class.java, + Factory(project = project, archiveName = archiveName, outputKind = outputKind), + ) + + /** Returns true if native code targeting [konanTarget] can be compiled on this host machine. */ + fun canCompileOnCurrentHost(konanTarget: KonanTarget) = hostManager.isEnabled(konanTarget) + + /** Calls the given [action] for each added [KonanTarget] in this compilation. */ + @Suppress("unused") // used in build.gradle + fun configureEachTarget(action: Action) { + nativeTargets.configureEach(action) + } + + /** + * Returns a [RegularFile] provider that points to the shared library output for the given + * [konanTarget]. + */ + fun sharedObjectOutputFor(konanTarget: KonanTarget): Provider { + return nativeTargets.named(konanTarget.name).flatMap { nativeTargetCompilation -> + nativeTargetCompilation.linkerTask.flatMap { it.clangParameters.outputFile } + } + } + + fun sharedArchiveOutputFor(konanTarget: KonanTarget): Provider { + return nativeTargets.named(konanTarget.name).flatMap { nativeTargetCompilation -> + nativeTargetCompilation.archiveTask.flatMap { it.llvmArchiveParameters.outputFile } + } + } + + /** + * Adds the given [konanTarget] to the list of compilation target if it can be built on this + * machine. The [action] block can be used to further configure the parameters of that + * compilation. + */ + @Suppress("MemberVisibilityCanBePrivate") // used in build.gradle + @JvmOverloads + fun configureTarget(konanTarget: KonanTarget, action: Action? = null) { + if (!canCompileOnCurrentHost(konanTarget)) { + // Cannot compile it on this host. This is similar to calling `ios` block in the build + // gradle file on a linux machine. + return + } + val nativeTarget = + if (nativeTargets.names.contains(konanTarget.name)) { + nativeTargets.named(konanTarget.name) + } else { + nativeTargets.register(konanTarget.name).also { + // force evaluation of target so that tasks are registered b/325518502 + nativeTargets.getByName(konanTarget.name) + } + } + if (action != null) { + nativeTarget.configure(action) + } + } + + /** + * Returns a provider for the given konan target and throws an exception if it is not + * registered. + */ + fun targetProvider(konanTarget: KonanTarget): Provider = + nativeTargets.named(konanTarget.name) + + /** + * Returns a provider that contains the list of [NativeTargetCompilation]s that matches the + * given [predicate]. + * + * You can use this provider to obtain the compilation for targets needed without forcing the + * creation of all other targets. + */ + internal fun targetsProvider( + predicate: (KonanTarget) -> Boolean + ): Provider> = + project.provider { + nativeTargets.names + .filter { predicate(SerializableKonanTarget(it).asKonanTarget) } + .map { nativeTargets.getByName(it) } + } + + /** Returns true if the given [konanTarget] is configured as a compilation target. */ + fun hasTarget(konanTarget: KonanTarget) = nativeTargets.names.contains(konanTarget.name) + + /** + * Convenience method to configure multiple targets at the same time. This is equal to calling + * [configureTarget] for each given [konanTargets]. + */ + @Suppress("unused") // used in build.gradle + @JvmOverloads + fun configureTargets( + konanTargets: List, + action: Action? = null, + ) = konanTargets.map { configureTarget(it, action) } + + /** + * Internal factory for creating instances of [nativeTargets]. This factory sets up all + * necessary inputs and their tasks for the native target. + */ + private class Factory( + private val project: Project, + private val archiveName: String, + private val outputKind: LinkerOutputKind, + ) : NamedDomainObjectFactory { + /** Shared task prefix for this archive */ + private val taskPrefix = "nativeCompilationFor".appendCapitalized(archiveName) + + /** Shared output directory prefix for tasks of this archive. */ + private val outputDir = + project.layout.buildDirectory.dir("clang".appendCapitalized(archiveName)) + + override fun create(name: String): NativeTargetCompilation { + return create(SerializableKonanTarget(name)) + } + + @JvmName("createWithSerializableKonanTarget") + private fun create( + serializableKonanTarget: SerializableKonanTarget + ): NativeTargetCompilation { + val includes = project.objects.fileCollection() + val sources = project.objects.fileCollection() + val freeArgs = project.objects.listProperty() + val linkedObjects = project.objects.fileCollection() + val linkerArgs = project.objects.listProperty() + val compileTask = + createCompileTask(serializableKonanTarget, includes, sources, freeArgs) + val archiveTask = createArchiveTask(serializableKonanTarget, compileTask) + val sharedLibTask = + createLinkerTask(serializableKonanTarget, compileTask, linkedObjects, linkerArgs) + return NativeTargetCompilation( + project = project, + konanTarget = serializableKonanTarget.asKonanTarget, + compileTask = compileTask, + archiveTask = archiveTask, + linkerTask = sharedLibTask, + sources = sources, + includes = includes, + linkedObjects = linkedObjects, + linkerArgs = linkerArgs, + freeArgs = freeArgs, + ) + } + + private fun createArchiveTask( + serializableKonanTarget: SerializableKonanTarget, + compileTask: TaskProvider, + ): TaskProvider { + val archiveTaskName = + taskPrefix.appendCapitalized("archive", serializableKonanTarget.name) + val archiveTask = + project.tasks.register(archiveTaskName, ClangArchiveTask::class.java) { task -> + val konanTarget = serializableKonanTarget.asKonanTarget + val archiveFileName = + listOf( + konanTarget.family.staticPrefix, + archiveName, + ".", + konanTarget.family.staticSuffix, + ) + .joinToString("") + task.usesService(KonanBuildService.obtain(project)) + task.llvmArchiveParameters.let { llvmAr -> + llvmAr.outputFile.set( + outputDir.map { it.file("$serializableKonanTarget/$archiveFileName") } + ) + llvmAr.konanTarget.set(serializableKonanTarget) + llvmAr.objectFiles.from(compileTask.map { it.clangParameters.output }) + } + } + return archiveTask + } + + private fun createCompileTask( + serializableKonanTarget: SerializableKonanTarget, + includes: ConfigurableFileCollection?, + sources: ConfigurableFileCollection?, + freeArgs: ListProperty, + ): TaskProvider { + val compileTaskName = + taskPrefix.appendCapitalized("compile", serializableKonanTarget.name) + val compileTask = + project.tasks.register(compileTaskName, ClangCompileTask::class.java) { compileTask + -> + compileTask.usesService(KonanBuildService.obtain(project)) + compileTask.clangParameters.let { clang -> + clang.output.set( + outputDir.map { it.dir("compile/$serializableKonanTarget") } + ) + includes?.let { clang.includes.from(it) } + sources?.let { clang.sources.from(it) } + clang.freeArgs.addAll(freeArgs) + clang.konanTarget.set(serializableKonanTarget) + } + } + return compileTask + } + + private fun createLinkerTask( + serializableKonanTarget: SerializableKonanTarget, + compileTask: TaskProvider, + linkedObjects: ConfigurableFileCollection, + linkerArgs: ListProperty, + ): TaskProvider { + val archiveTaskName = + taskPrefix.appendCapitalized("runLinker", serializableKonanTarget.name) + val archiveTask = + project.tasks.register(archiveTaskName, ClangLinkerTask::class.java) { task -> + val konanTarget = serializableKonanTarget.asKonanTarget + + val archiveFileName = + if (outputKind == LinkerOutputKind.EXECUTABLE) { + archiveName + } else { + listOf( + konanTarget.family.dynamicPrefix, + archiveName, + ".", + konanTarget.family.dynamicSuffix, + ) + .joinToString("") + } + + task.usesService(KonanBuildService.obtain(project)) + task.clangParameters.let { clang -> + clang.outputFile.set( + outputDir.map { it.file("$serializableKonanTarget/$archiveFileName") } + ) + clang.linkerOutputKind.set(outputKind) + clang.konanTarget.set(serializableKonanTarget) + clang.objectFiles.from(compileTask.map { it.clangParameters.output }) + clang.linkedObjects.from(linkedObjects) + clang.linkerArgs.addAll(linkerArgs) + } + } + return archiveTask + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt new file mode 100644 index 0000000000000..c60068e60fc64 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeLibraryBundler.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import androidx.build.androidExtension +import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget +import com.android.build.api.variant.HasDeviceTests +import com.android.build.api.variant.SourceDirectories +import com.android.build.api.variant.Sources +import com.android.utils.appendCapitalized +import org.gradle.api.Project +import org.gradle.kotlin.dsl.get +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget +import org.jetbrains.kotlin.konan.target.Family + +/** + * Helper class to bundle outputs of [MultiTargetNativeCompilation] with a JVM or Android project. + */ +class NativeLibraryBundler(private val project: Project) { + /** + * Adds the shared library outputs from [nativeCompilation] to the resources of the [jvmTarget]. + * + * @see CombineObjectFilesTask for details. + */ + fun addNativeLibrariesToResources( + jvmTarget: KotlinJvmTarget, + nativeCompilation: MultiTargetNativeCompilation, + compilationName: String = KotlinCompilation.MAIN_COMPILATION_NAME, + ) { + val combineTask = + project.tasks.register( + "createCombinedResourceArchiveFor" + .appendCapitalized( + jvmTarget.name, + nativeCompilation.archiveName, + compilationName, + ), + CombineObjectFilesTask::class.java, + ) { + it.outputDirectory.set( + project.layout.buildDirectory.dir( + "combinedNativeLibraries/${jvmTarget.name}/" + + "${nativeCompilation.archiveName}/$compilationName" + ) + ) + } + val jniFamilies = listOf(Family.OSX, Family.MINGW, Family.LINUX) + combineTask.configureFrom(nativeCompilation) { it.family in jniFamilies } + jvmTarget.compilations[compilationName] + .defaultSourceSet + .resources + .srcDir(combineTask.map { it.outputDirectory }) + } + + /** + * Adds the shared library outputs from [nativeCompilation] to a given variant src set of the + * [androidTarget], expressed with the [provideSourceDirectories]. + * + * @see CombineObjectFilesTask for details. + */ + fun addNativeLibrariesToAndroidVariantSources( + androidTarget: KotlinMultiplatformAndroidLibraryTarget, + nativeCompilation: MultiTargetNativeCompilation, + forTest: Boolean, + provideSourceDirectories: Sources.() -> (SourceDirectories.Layered?), + ) { + project.androidExtension.onVariants(project.androidExtension.selector().all()) { variant -> + fun setup(name: String, sources: SourceDirectories.Layered?) { + checkNotNull(sources) { + "Cannot find jni libs sources for variant: $variant (forTest=$forTest)" + } + val combineTask = + project.tasks.register( + "createJniLibsDirectoryFor" + .appendCapitalized( + nativeCompilation.archiveName, + "for", + name, + androidTarget.name, + ), + CombineObjectFilesTask::class.java, + ) + combineTask.configureFrom(nativeCompilation) { it.family == Family.ANDROID } + + sources.addGeneratedSourceDirectory( + taskProvider = combineTask, + wiredWith = { it.outputDirectory }, + ) + } + + if (forTest) { + check(variant is HasDeviceTests) { "Variant $variant does not have a test target" } + variant.deviceTests.forEach { (_, deviceTest) -> + setup(deviceTest.name, provideSourceDirectories(deviceTest.sources)) + } + } else { + setup(variant.name, provideSourceDirectories(variant.sources)) + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeTargetCompilation.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeTargetCompilation.kt new file mode 100644 index 0000000000000..cbdce7aea0530 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeTargetCompilation.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import androidx.build.ProjectLayoutType +import java.io.File +import org.gradle.api.Named +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.konan.target.Family +import org.jetbrains.kotlin.konan.target.KonanTarget + +/** + * Represents a C compilation for a single [konanTarget]. + * + * @param konanTarget Target host for the compilation. + * @param compileTask The task that compiles the sources and build .o file for each source file. + * @param archiveTask The task that will archive the output of the [compileTask] into a single .a + * file. + * @param linkerTask The task that will created a shared library from the output of [compileTask] + * that also optionally links with [linkedObjects] + * @param sources List of source files for the compilation. + * @param includes List of include directories containing .h files for the compilation. + * @param linkedObjects List of object files that should be dynamically linked in the final shared + * object output. + * @param linkerArgs Arguments that will be passed into linker when creating a shared library. + * @param freeArgs Arguments that will be passed into clang for compilation. + */ +class NativeTargetCompilation +internal constructor( + val project: Project, + val konanTarget: KonanTarget, + internal val compileTask: TaskProvider, + internal val archiveTask: TaskProvider, + internal val linkerTask: TaskProvider, + val sources: ConfigurableFileCollection, + val includes: ConfigurableFileCollection, + val linkedObjects: ConfigurableFileCollection, + @Suppress("unused") // used via build.gradle + val linkerArgs: ListProperty, + @Suppress("unused") // used via build.gradle + val freeArgs: ListProperty, +) : Named { + override fun getName(): String = konanTarget.name + + /** + * Dynamically links the shared library output of this target with the given [dependency]'s + * object library output. + */ + @Suppress("unused") // used from build.gradle + fun linkWith(dependency: MultiTargetNativeCompilation) { + linkedObjects.from(dependency.sharedObjectOutputFor(konanTarget)) + } + + /** + * Statically include the shared library output of this target with the given [dependency]'s + * archive library output. + */ + @Suppress("unused") // used from build.gradle + fun include(dependency: MultiTargetNativeCompilation) { + linkedObjects.from(dependency.sharedArchiveOutputFor(konanTarget)) + } + + /** Convenience method to add jni headers to the compilation. */ + @Suppress("unused") // used from build.gradle + fun addJniHeaders() { + if (konanTarget.family == Family.ANDROID) { + // android already has JNI + return + } + + includes.from(project.provider { findJniHeaderDirectories() }) + } + + private fun findJniHeaderDirectories(): List { + // TODO b/306669673 add support for GitHub builds. + // we need to find 2 jni header files + // jni.h -> This is the same across all platforms + // jni_md.h -> Includes machine dependant definitions. + // Internal Devs: You can read more about it here: http://go/androidx-jni-cross-compilation + val javaHome = File(System.getProperty("java.home")) + if (ProjectLayoutType.isPlayground(project)) { + return findJniHeadersInPlayground(javaHome) + } + // for jni_md, we need to find the prebuilts because each jdk ships with jni_md only for + // its own target family. + val jdkPrebuiltsRoot = javaHome.parentFile + + val relativeHeaderPaths = + when (konanTarget.family) { + Family.MINGW -> { + listOf("windows-x86/include", "windows-x86/include/win32") + } + Family.OSX -> { + // it is OK that we are using arm64 here, they are the same files (openjdk only + // distinguishes between unix and windows). + listOf("darwin-arm64/include", "darwin-arm64/include/darwin") + } + Family.LINUX -> { + listOf("linux-x86/include", "linux-x86/include/linux") + } + else -> error("unsupported family ($konanTarget) for JNI compilation") + } + return relativeHeaderPaths + .map { jdkPrebuiltsRoot.resolve(it) } + .onEach { + check(it.exists()) { + "Cannot find header directory (${it.name}) in ${it.canonicalPath}" + } + } + } + + /** + * JDK ships with JNI headers only for the current platform. As a result, we don't have access + * to cross-platform jni headers. They are mostly the same and we don't ship cross compiled code + * from GitHub so it is acceptable to use local JNI headers for cross platform compilation on + * GitHub. + */ + private fun findJniHeadersInPlayground(javaHome: File): List { + val include = File(javaHome, "include") + if (!include.exists()) { + error("Cannot find header directory in $javaHome") + } + return listOf( + include, + File(include, "darwin"), + File(include, "linux"), + File(include, "win32"), + ) + .filter { it.exists() } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/README.md b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/README.md new file mode 100644 index 0000000000000..904082143a2c8 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/README.md @@ -0,0 +1,47 @@ +# Clang Compilation + +This package includes classes to compile C code using the Clang compiler distributed +in the Kotlin Native prebuilts. + +Public API of this functionality is exported to build.gradle files via +`AndroidXMultiplatformExtension` to limit usages to KMP project. + +There are 2 primary functionalities: + +## Compiling C code with multiple targets: +`AndroidXMultiplatformExtension.createNativeCompilation` can be used to create a +`MultiTargetNativeCompilation` instance. `MultiTargetNativeCompilation` is the abstraction used to +define a C compilation that has sources, includes, dependencies and multiple Konan targets. + +Unlike the CMake build, this compilation is fully compatible with Gradle build cache. + +Once the compilation is created, it can be linked to the artifacts in 2 different ways: + +### CInterop: +`AndroidXMultiplatformExtension.createCinterop` can be used to configure the build to compile the +given `MultiTargetNativeCompilation` and embed it into the klib via +[cinterop](https://kotlinlang.org/docs/native-c-interop.html). +The C code will be compiled per Konan target and the output will be embedded into the generated +klib. + +* Note: Due to the limitation of CInterop requiring a DEF file with static library paths, CInterop + compilation relies on relative paths between the source code and build output, hence the cache may + not be fully move-able (see: KT-62800, KT-62795). + +### Java Resources / Android JNI: +`AndroidXMultiplatformExtension.addNativeLibrariesToJniLibs` / `addNativeLibrariesToResources` can +be used to bundle the native code as a shared library inside java resources for JVM and `jnilibs` +for Android. This allows using the compiled library via regular JNI bridges. + +## Clang vs CMake +This solution is initially created due to the Gradle build cache incompatibility of CMake. Konan +native compilation provides a decent alternative because Kotlin Native ships all necessary +multiplatform dependencies as 1 zip file (e.g. sysroots) along with Clang compiler. For native code, +we are only interested in platforms supported by Kotlin Native, hence this alignment is future +proof in case the set of platforms changes in the future. This is also another reason why the usages +of these APIs are limited to KMP projects. + +Once the CMake cacheability problem is fixed, it should be possible to get rid of the Clang +compilation tasks if necessary cross-compilation dependecies can be obtained by other means. + +You can read more about the design here: http://go/androidx-clang (internal only). diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/SerializableKonanTarget.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/SerializableKonanTarget.kt new file mode 100644 index 0000000000000..bf1b20e3e43a8 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/clang/SerializableKonanTarget.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.clang + +import java.io.Serializable +import org.jetbrains.kotlin.konan.target.KonanTarget + +/** + * We cannot use KonanTarget as Gradle input/output due to + * https://youtrack.jetbrains.com/issue/KT-61657. Hence, we have this value class which represents + * it as a string. + */ +@JvmInline +value class SerializableKonanTarget(val name: String) : Serializable { + init { + check(KonanTarget.predefinedTargets.contains(name)) { "Invalid KonanTarget name: $name" } + } + + val asKonanTarget + get(): KonanTarget { + return KonanTarget.predefinedTargets[name] + ?: error("No KonanTarget found with name $name") + } + + override fun toString() = name + + constructor(konanTarget: KonanTarget) : this(konanTarget.name) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt new file mode 100644 index 0000000000000..71176b6b58460 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt @@ -0,0 +1,401 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dackka + +import androidx.build.docs.ProjectStructureMetadata +import com.google.gson.GsonBuilder +import java.io.File +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.process.ExecOperations +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor + +@CacheableTask +abstract class DackkaTask +@Inject +constructor(private val workerExecutor: WorkerExecutor, private val objects: ObjectFactory) : + DefaultTask() { + + @get:OutputFile abstract val argsJsonFile: RegularFileProperty + + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val projectStructureMetadataFile: RegularFileProperty + + // Classpath containing Dackka + @get:Classpath abstract val dackkaClasspath: ConfigurableFileCollection + + // Classpath containing dependencies of libraries needed to resolve types in docs + @get:[InputFiles Classpath] + abstract val dependenciesClasspath: ConfigurableFileCollection + + // Directory containing the code samples from framework + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val frameworkSamplesDir: DirectoryProperty + + // Directory containing the code samples for non-KMP libraries + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val samplesJvmDir: DirectoryProperty + + // Directory containing the code samples for KMP libraries + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val samplesKmpDir: DirectoryProperty + + // Directory containing the JVM source code for Dackka to process + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val jvmSourcesDir: DirectoryProperty + + // Directory containing the multiplatform source code for Dackka to process + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val multiplatformSourcesDir: DirectoryProperty + + // Directory containing the package-lists + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val projectListsDirectory: DirectoryProperty + + // Location of generated reference docs + @get:OutputDirectory abstract val destinationDir: DirectoryProperty + + // Set of packages to exclude for refdoc generation for all languages + @get:Input abstract val excludedPackages: SetProperty + + // Set of packages to exclude for Java refdoc generation + @get:Input abstract val excludedPackagesForJava: SetProperty + + // Set of packages to exclude for Kotlin refdoc generation + @get:Input abstract val excludedPackagesForKotlin: SetProperty + + @get:Input abstract val annotationsNotToDisplay: ListProperty + + @get:Input abstract val annotationsNotToDisplayJava: ListProperty + + @get:Input abstract val annotationsNotToDisplayKotlin: ListProperty + + @get:Input abstract val hidingAnnotations: ListProperty + + @get:Input abstract val nullabilityAnnotations: ListProperty + + // Version metadata for apiSince, only marked as @InputFiles if includeVersionMetadata is true + @get:Internal abstract val versionMetadataFiles: ConfigurableFileCollection + + @InputFiles + @PathSensitive(PathSensitivity.NONE) + fun getOptionalVersionMetadataFiles(): ConfigurableFileCollection { + return if (includeVersionMetadata) { + versionMetadataFiles + } else { + objects.fileCollection() + } + } + + // Maps to the system variable LIBRARY_METADATA_FILE containing artifactID and other metadata + @get:[InputFile PathSensitive(PathSensitivity.NONE)] + abstract val libraryMetadataFile: RegularFileProperty + + // The base URLs to create source links for classes, functions, and properties, respectively, as + // format strings with placeholders for the file path and qualified class name, function name, + // or property name. + @get:Input abstract val baseSourceLink: Property + @get:Input abstract val baseFunctionSourceLink: Property + @get:Input abstract val basePropertySourceLink: Property + + /** + * Option for whether to include apiSince metadata in the docs. Defaults to including metadata. + * Run with `--no-version-metadata` to avoid running `generateApi` before `docs`. + */ + @get:Input + @set:Option( + option = "version-metadata", + description = "Include added-in/deprecated-in API version metadata", + ) + var includeVersionMetadata: Boolean = true + + private fun sourceSets(): List { + fun getSampleSourceFileCollection(): FileCollection { + // Filter out non-existent directories as Dackka crashes if you pass it in b/332262321 + val dirs = + listOf(samplesJvmDir, samplesKmpDir, frameworkSamplesDir).mapNotNull { + if (it.get().asFile.exists()) it else null + } + return objects.fileCollection().from(dirs) + } + val externalDocs = + externalLinks.map { (name, url) -> + DokkaInputModels.GlobalDocsLink( + url = url, + packageListUrl = + "file://${ + projectListsDirectory.get().asFile.absolutePath + }/$name/package-list", + ) + } + val gson = GsonBuilder().create() + val multiplatformSourceSets = + projectStructureMetadataFile + .get() + .asFile + .takeIf { it.exists() } + ?.let { metadataFile -> + val metadata = + gson.fromJson(metadataFile.readText(), ProjectStructureMetadata::class.java) + // Sort to ensure that child sourceSets come after their parents, b/404784813 + metadata.sourceSets + .sortedWith(compareBy({ it.dependencies.size }, { it.name })) + .mapNotNull { sourceSet -> + val sourceDir = + multiplatformSourcesDir.get().asFile.resolve(sourceSet.name) + if (!sourceDir.exists()) return@mapNotNull null + val analysisPlatform = + DokkaAnalysisPlatform.valueOf( + sourceSet.analysisPlatform.uppercase() + ) + DokkaInputModels.SourceSet( + id = sourceSetIdForSourceSet(sourceSet.name), + displayName = sourceSet.name, + analysisPlatform = analysisPlatform.jsonName, + sourceRoots = objects.fileCollection().from(sourceDir), + // TODO(b/181224204): KMP samples aren't supported, dackka assumes + // all + // samples are in common + samples = + if (analysisPlatform == DokkaAnalysisPlatform.COMMON) { + getSampleSourceFileCollection() + } else { + objects.fileCollection() + }, + includes = objects.fileCollection().from(includesFiles(sourceDir)), + classpath = dependenciesClasspath, + externalDocumentationLinks = externalDocs, + dependentSourceSets = + sourceSet.dependencies.map { sourceSetIdForSourceSet(it) }, + noJdkLink = !analysisPlatform.androidOrJvm(), + noAndroidSdkLink = + analysisPlatform != DokkaAnalysisPlatform.ANDROID, + noStdlibLink = false, + // Dackka source link configuration doesn't use the Dokka version + sourceLinks = emptyList(), + ) + } + } ?: emptyList() + return listOf( + DokkaInputModels.SourceSet( + id = sourceSetIdForSourceSet("main"), + displayName = "main", + analysisPlatform = "jvm", + sourceRoots = objects.fileCollection().from(jvmSourcesDir), + samples = getSampleSourceFileCollection(), + includes = objects.fileCollection().from(includesFiles(jvmSourcesDir.get().asFile)), + classpath = dependenciesClasspath, + externalDocumentationLinks = externalDocs, + dependentSourceSets = emptyList(), + noJdkLink = false, + noAndroidSdkLink = false, + noStdlibLink = false, + // Dackka source link configuration doesn't use the Dokka version + sourceLinks = emptyList(), + ) + ) + multiplatformSourceSets + } + + // Documentation for Dackka command line usage and arguments can be found at + // https://kotlin.github.io/dokka/1.6.0/user_guide/cli/usage/ + // Documentation for the DevsitePlugin arguments can be found at + // https://cs.android.com/androidx/platform/tools/dokka-devsite-plugin/+/master:src/main/java/com/google/devsite/DevsiteConfiguration.kt + private fun computeArguments(): File { + val gson = DokkaUtils.createGson() + val linksConfiguration = "" + val jsonMap = + mapOf( + "outputDir" to destinationDir.get().asFile.path, + "globalLinks" to linksConfiguration, + "sourceSets" to sourceSets(), + "offlineMode" to "true", + "noJdkLink" to "true", + "pluginsConfiguration" to + listOf( + mapOf( + "fqPluginName" to "com.google.devsite.DevsitePlugin", + "serializationFormat" to "JSON", + // values is a JSON string + "values" to + gson.toJson( + mapOf( + "projectPath" to "androidx", + "javaDocsPath" to "", + "kotlinDocsPath" to "kotlin", + "excludedPackages" to excludedPackages.get(), + "excludedPackagesForJava" to excludedPackagesForJava.get(), + "excludedPackagesForKotlin" to + excludedPackagesForKotlin.get(), + "libraryMetadataFilename" to + libraryMetadataFile.get().toString(), + "baseSourceLink" to baseSourceLink.get(), + "baseFunctionSourceLink" to baseFunctionSourceLink.get(), + "basePropertySourceLink" to basePropertySourceLink.get(), + "annotationsNotToDisplay" to annotationsNotToDisplay.get(), + "annotationsNotToDisplayJava" to + annotationsNotToDisplayJava.get(), + "annotationsNotToDisplayKotlin" to + annotationsNotToDisplayKotlin.get(), + "hidingAnnotations" to hidingAnnotations.get(), + "versionMetadataFilenames" to getVersionMetadataFiles(), + "validNullabilityAnnotations" to + nullabilityAnnotations.get(), + ) + ), + ) + ), + ) + + val json = gson.toJson(jsonMap) + return argsJsonFile.get().asFile.apply { writeText(json) } + } + + /** + * If version metadata shouldn't be included in the docs, returns an empty list. Otherwise, + * returns the list of version metadata files after checking if they're all JSON. If version + * metadata does not exist for a project, it's possible that a configuration which isn't an + * exact match of the version metadata attributes to be selected as version metadata. + */ + private fun getVersionMetadataFiles(): List { + val (json, nonJson) = + getOptionalVersionMetadataFiles().files.partition { it.extension == "json" } + if (nonJson.isNotEmpty()) { + logger.error( + "The following were resolved as version metadata files but are not JSON files. " + + "If these projects do not have API tracking enabled (e.g. compiler plugin, " + + "annotation processor, proto), they should not be included in the docs. " + + "Remove the projects from `docs-public/build.gradle` and/or " + + "`docs-tip-of-tree/build.gradle`.\n" + + nonJson.joinToString("\n") + ) + } + return json + } + + @TaskAction + fun generate() { + runDackkaWithArgs( + classpath = dackkaClasspath, + argsFile = computeArguments(), + workerExecutor = workerExecutor, + ) + } + + companion object { + private val externalLinks = + mapOf( + "coroutinesCore" to "https://kotlinlang.org/api/kotlinx.coroutines/", + "android" to "https://developer.android.com/reference", + "guava" to "https://guava.dev/releases/18.0/api/docs/", + "kotlin" to "https://kotlinlang.org/api/core/kotlin-stdlib/", + "junit" to "https://junit.org/junit4/javadoc/4.12/", + "okio" to "https://square.github.io/okio/3.x/okio/", + "protobuf" to "https://protobuf.dev/reference/java/api-docs/", + "kotlinpoet" to "https://square.github.io/kotlinpoet/1.x/kotlinpoet/", + "skiko" to "https://jetbrains.github.io/skiko/", + "reactivex" to "https://reactivex.io/RxJava/2.x/javadoc/", + "reactivex-rxjava3" to "http://reactivex.io/RxJava/3.x/javadoc/", + "grpc" to "https://grpc.github.io/grpc-java/javadoc/", + // From developer.android.com/reference/com/google/android/play/core/package-list + "play" to "https://developer.android.com/reference/", + // From developer.android.com/reference/com/google/android/material/package-list + "material" to "https://developer.android.com/reference", + "okhttp3" to "https://square.github.io/okhttp/5.x/", + "truth" to "https://truth.dev/api/0.41/", + // From developer.android.com/reference/android/support/wearable/package-list + "wearable" to "https://developer.android.com/reference/", + // Filtered to just java.awt and javax packages (base java packages are included in + // the android package-list) + "javase8" to "https://docs.oracle.com/javase/8/docs/api/", + "javaee7" to "https://docs.oracle.com/javaee%2F7%2Fapi%2F%2F", + "findbugs" to "https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/latest/", + // All package-lists below were created manually + "mlkit" to "https://developers.google.com/android/reference/", + "dagger" to "https://dagger.dev/api/latest/", + "reactivestreams" to + "https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/", + "jetbrains-annotations" to + "https://javadoc.io/doc/org.jetbrains/annotations/latest/", + "auto-value" to + "https://www.javadoc.io/doc/com.google.auto.value/auto-value/latest/", + "robolectric" to "https://robolectric.org/javadoc/4.11/", + "interactive-media" to + "https://developers.google.com/interactive-media-ads/docs/sdks/android/" + + "client-side/api/reference/com/google/ads/interactivemedia/v3", + "errorprone" to "https://errorprone.info/api/latest/", + "gms" to "https://developers.google.com/android/reference", + "checkerframework" to "https://checkerframework.org/api/", + "chromium" to + "https://developer.android.com/develop/connectivity/cronet/reference/", + "jspecify" to "https://jspecify.dev/docs/api/", + ) + } +} + +interface DackkaParams : WorkParameters { + val args: ListProperty + val classpath: SetProperty +} + +fun runDackkaWithArgs(classpath: FileCollection, argsFile: File, workerExecutor: WorkerExecutor) { + val workQueue = workerExecutor.noIsolation() + workQueue.submit(DackkaWorkAction::class.java) { parameters -> + parameters.args.set(listOf(argsFile.path, "-loggingLevel", "WARN")) + parameters.classpath.set(classpath) + } +} + +abstract class DackkaWorkAction @Inject constructor(private val execOperations: ExecOperations) : + WorkAction { + override fun execute() { + execOperations.javaexec { + it.mainClass.set("org.jetbrains.dokka.MainKt") + it.args = parameters.args.get() + it.classpath(parameters.classpath.get()) + } + } +} + +private fun includesFiles(sourceRoot: File): List { + return sourceRoot.walkTopDown().filter { it.name.endsWith("documentation.md") }.toList() +} + +private fun sourceSetIdForSourceSet(name: String): DokkaInputModels.SourceSetId { + return DokkaInputModels.SourceSetId(scopeId = "androidx", sourceSetName = name) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DokkaInputModels.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DokkaInputModels.kt new file mode 100644 index 0000000000000..72c30a51abc6d --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DokkaInputModels.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") // used by gson + +package androidx.build.dackka + +import com.google.gson.annotations.SerializedName +import java.io.File +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Nested +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity + +// These are models used to invoke dokka from the command line. +// Most of these models are identical to +// https://github.com/Kotlin/dokka/blob/master/core/src/main/kotlin/configuration.kt +// with the caveat that they have Gradle task input annotations when necessary. + +internal object DokkaInputModels { + class SourceSet( + @get:Input val displayName: String, + @get:Nested @SerializedName("sourceSetID") val id: SourceSetId, + @Classpath val classpath: FileCollection, + @get:InputFiles @PathSensitive(PathSensitivity.RELATIVE) val sourceRoots: FileCollection, + @get:InputFiles @PathSensitive(PathSensitivity.RELATIVE) val samples: FileCollection, + @get:InputFiles @PathSensitive(PathSensitivity.RELATIVE) val includes: FileCollection, + @get:Input val analysisPlatform: String, + @get:Input val documentedVisibilities: List = listOf("PUBLIC", "PROTECTED"), + @get:Input val noStdlibLink: Boolean, + @get:Input val noJdkLink: Boolean, + @get:Input val noAndroidSdkLink: Boolean, + @Nested val dependentSourceSets: List, + @Nested val externalDocumentationLinks: List, + @Nested val sourceLinks: List, + ) + + class SourceSetId(@get:Input val sourceSetName: String, @get:Input val scopeId: String) + + class SrcLink( + @get:InputDirectory @PathSensitive(PathSensitivity.RELATIVE) val localDirectory: File, + @get:Input val remoteUrl: String, + @get:Input val remoteLineSuffix: String = ";l=", + ) + + class GlobalDocsLink(@get:Input val url: String, @get:Input val packageListUrl: String?) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DokkaUtils.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DokkaUtils.kt new file mode 100644 index 0000000000000..5a25207cd5eaa --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/DokkaUtils.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dackka + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import java.io.File +import java.lang.reflect.Type +import org.gradle.api.file.FileCollection +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget + +internal object DokkaUtils { + /** Creates a GSON instance that can be used to serialize Dokka CLI json models. */ + fun createGson(): Gson = + GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(File::class.java, CanonicalFileSerializer()) + .registerTypeAdapter(FileCollection::class.java, FileCollectionSerializer()) + .create() + + /** Serializer for Gradle's [FileCollection] */ + private class FileCollectionSerializer : JsonSerializer { + override fun serialize( + src: FileCollection, + typeOfSrc: Type, + context: JsonSerializationContext, + ): JsonElement { + return context.serialize(src.files) + } + } + + /** + * Serializer for [File] instances in the Dokka CLI model. + * + * Dokka doesn't work well with relative paths hence we use a canonical paths while setting up + * its parameters. + */ + private class CanonicalFileSerializer : JsonSerializer { + override fun serialize( + src: File, + typeOfSrc: Type, + context: JsonSerializationContext, + ): JsonElement { + return JsonPrimitive(src.canonicalPath) + } + } +} + +enum class DokkaAnalysisPlatform(val jsonName: String) { + JVM("jvm"), + ANDROID("jvm"), // intentionally same as JVM as dokka only support jvm + JS("js"), + NATIVE("native"), + COMMON("common"); + + fun androidOrJvm() = this == JVM || this == ANDROID +} + +fun KotlinTarget.docsPlatform() = + when (platformType) { + KotlinPlatformType.common -> DokkaAnalysisPlatform.COMMON + KotlinPlatformType.jvm -> DokkaAnalysisPlatform.JVM + KotlinPlatformType.js -> DokkaAnalysisPlatform.JS + KotlinPlatformType.wasm -> DokkaAnalysisPlatform.JS + KotlinPlatformType.androidJvm -> DokkaAnalysisPlatform.ANDROID + KotlinPlatformType.native -> DokkaAnalysisPlatform.NATIVE + } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/GenerateMetadataTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/GenerateMetadataTask.kt new file mode 100644 index 0000000000000..5d1b718620532 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/GenerateMetadataTask.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dackka + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import java.io.File +import java.io.FileWriter +import java.util.zip.ZipFile +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier + +@CacheableTask +abstract class GenerateMetadataTask : DefaultTask() { + + /** List of artifacts to convert to JSON */ + @Input abstract fun getArtifactIds(): ListProperty + + /** List of files corresponding to artifacts in [getArtifactIds] */ + @InputFiles + @PathSensitive(PathSensitivity.NONE) + abstract fun getArtifactFiles(): ListProperty + + /** List of multiplatform artifacts to convert to JSON */ + @Input abstract fun getMultiplatformArtifactIds(): ListProperty + + /** List of files corresponding to artifacts in [getMultiplatformArtifactIds] */ + @InputFiles + @PathSensitive(PathSensitivity.NONE) + abstract fun getMultiplatformArtifactFiles(): ListProperty + + /** Location of the generated JSON file */ + @get:OutputFile abstract val destinationFile: RegularFileProperty + + @TaskAction + fun generate() { + val entries = + createEntries(getArtifactIds().get(), getArtifactFiles().get(), multiplatform = false) + + createEntries( + getMultiplatformArtifactIds().get(), + getMultiplatformArtifactFiles().get(), + multiplatform = true, + ) + + val gson = + if (DEBUG) { + GsonBuilder().setPrettyPrinting().create() + } else { + Gson() + } + val writer = FileWriter(destinationFile.get().toString()) + gson.toJson(entries, writer) + writer.close() + } + + private fun createEntries( + ids: List, + artifacts: List, + multiplatform: Boolean, + ): List = + ids.indices.mapNotNull { i -> + val id = ids[i] + val file = artifacts[i] + // Only process artifact if it can be cast to ModuleComponentIdentifier. + // + // In practice, metadata is generated only for docs-public and not docs-tip-of-tree + // (where id.componentIdentifier is DefaultProjectComponentIdentifier). + if (id.componentIdentifier !is DefaultModuleComponentIdentifier) return@mapNotNull null + + // Created https://github.com/gradle/gradle/issues/21415 to track surfacing + // group / module / version in ComponentIdentifier + val componentId = (id.componentIdentifier as ModuleComponentIdentifier) + + // Fetch the list of files contained in the .jar file + val fileList = + ZipFile(file).entries().toList().map { + if (multiplatform) { + // Paths for multiplatform will start with a directory for the platform + // (e.g. + // "commonMain"), while Dackka only sees the part of the path after this. + it.name.substringAfter("/") + } else { + it.name + } + } + + MetadataEntry( + groupId = componentId.group, + artifactId = componentId.module, + releaseNotesUrl = generateReleaseNotesUrl(componentId.group), + jarContents = fileList, + ) + } + + private fun generateReleaseNotesUrl(groupId: String): String { + val library = groupId.removePrefix("androidx.").replace(".", "-") + return "/jetpack/androidx/releases/$library" + } + + companion object { + private const val DEBUG = false + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/MetadataEntry.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/MetadataEntry.kt new file mode 100644 index 0000000000000..da9d2de2b6738 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/MetadataEntry.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dackka + +import com.google.gson.annotations.SerializedName + +/** Helper data class to store the metadata information for each library/path. */ +data class MetadataEntry( + @SerializedName("groupId") val groupId: String, + @SerializedName("artifactId") val artifactId: String, + @SerializedName("releaseNotesUrl") val releaseNotesUrl: String, + @SerializedName("jarContents") val jarContents: List, +) diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS new file mode 100644 index 0000000000000..ef873ccc99491 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS @@ -0,0 +1,3 @@ +asfalcone@google.com +fsladkey@google.com +juliamcclellan@google.com diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt new file mode 100644 index 0000000000000..4c924213847e0 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt @@ -0,0 +1,553 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dependencyTracker + +import androidx.build.dependencyTracker.AffectedModuleDetector.Companion.ENABLE_ARG +import androidx.build.getCheckoutRoot +import androidx.build.getDistributionDirectory +import androidx.build.gitclient.getChangedFilesProvider +import androidx.build.gradle.isRoot +import java.io.File +import org.gradle.api.Action +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.ProjectDependency +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.invocation.Gradle +import org.gradle.api.logging.Logger +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters +import org.gradle.api.services.BuildServiceSpec + +/** + * The subsets we allow the projects to be partitioned into. This is to allow more granular testing. + * Specifically, to enable running large tests on CHANGED_PROJECTS, while still only running small + * and medium tests on DEPENDENT_PROJECTS. + * + * The ProjectSubset specifies which projects we are interested in testing. The + * AffectedModuleDetector determines the minimum set of projects that must be built in order to run + * all the tests along with their runtime dependencies. + * + * The subsets are: CHANGED_PROJECTS -- The containing projects for any files that were changed in + * this CL. + * + * DEPENDENT_PROJECTS -- Any projects that have a dependency on any of the projects in the + * CHANGED_PROJECTS set. + * + * NONE -- A status to return for a project when it is not supposed to be built. + */ +enum class ProjectSubset { + DEPENDENT_PROJECTS, + CHANGED_PROJECTS, + NONE, +} + +/** + * A utility class that can discover which files are changed based on git history. + * + * To enable this, you need to pass [ENABLE_ARG] into the build as a command line parameter + * (-P) + * + * Currently, it checks git logs to find last merge CL to discover where the anchor CL is. + * + * Eventually, we'll move to the props passed down by the build system when it is available. + * + * Since this needs to check project dependency graph to work, it cannot be accessed before all + * projects are loaded. Doing so will throw an exception. + */ +abstract class AffectedModuleDetector(protected val logger: Logger?) { + /** Returns whether this project was affected by current changes. */ + abstract fun shouldInclude(project: String): Boolean + + /** Returns whether this task was affected by current changes. */ + open fun shouldInclude(task: Task): Boolean { + val projectPath = getProjectPathFromTaskPath(task.path) + val include = shouldInclude(projectPath) + val inclusionVerb = if (include) "Including" else "Excluding" + logger?.info("$inclusionVerb task ${task.path}") + return include + } + + /** + * Returns the set that the project belongs to. The set is one of the ProjectSubset above. This + * is used by the test config generator. + */ + abstract fun getSubset(projectPath: String): ProjectSubset + + fun getProjectPathFromTaskPath(taskPath: String): String { + val lastColonIndex = taskPath.lastIndexOf(":") + val projectPath = taskPath.substring(0, lastColonIndex) + return projectPath + } + + companion object { + private const val ROOT_PROP_NAME = "affectedModuleDetector" + private const val SERVICE_NAME = ROOT_PROP_NAME + "BuildService" + private const val LOG_FILE_NAME = "affected_module_detector_log.txt" + const val ENABLE_ARG = "androidx.enableAffectedModuleDetection" + const val BASE_COMMIT_ARG = "androidx.affectedModuleDetector.baseCommit" + + @JvmStatic + fun configure(gradle: Gradle, rootProject: Project) { + // Make an AffectedModuleDetectorWrapper that callers can save before the real + // AffectedModuleDetector is ready. Callers won't be able to use it until the wrapped + // detector has been assigned, but configureTaskGuard can still reference it in + // closures that will execute during task execution. + val instance = AffectedModuleDetectorWrapper() + rootProject.extensions.add(ROOT_PROP_NAME, instance) + + val enabledProvider = rootProject.providers.gradleProperty(ENABLE_ARG) + val enabled = enabledProvider.isPresent && enabledProvider.get() != "false" + + val outputFile = rootProject.getDistributionDirectory().file(LOG_FILE_NAME) + + if (!enabled) { + val provider = + setupWithParams(rootProject) { spec -> + val params = spec.parameters + params.enabled.set(false) + params.acceptAll = true + params.logOutputFileProvider.set(outputFile) + } + instance.wrapped = provider + return + } + val baseCommitOverride: Provider = + rootProject.providers.gradleProperty(BASE_COMMIT_ARG) + + gradle.taskGraph.whenReady { + val projectGraph = ProjectGraph(rootProject) + val dependencyMap = mutableMapOf>() + rootProject.subprojects.forEach { project -> + project.configurations.forEach { config -> + config.dependencies.filterIsInstance().forEach { + dependency -> + dependencyMap + .getOrPut(dependency.path) { mutableSetOf() } + .add(project.path) + } + } + } + val provider = + setupWithParams(rootProject) { spec -> + val params = spec.parameters + params.rootDir = rootProject.projectDir + params.enabled.set(true) + params.dependencyMap.set(dependencyMap) + params.checkoutRoot = rootProject.getCheckoutRoot() + params.projectGraph = projectGraph + params.logOutputFileProvider.set(outputFile) + params.baseCommitOverride = baseCommitOverride + params.gitChangedFilesProvider = + rootProject.getChangedFilesProvider(baseCommitOverride) + } + instance.wrapped = provider + } + } + + private fun setupWithParams( + rootProject: Project, + configureAction: Action>, + ): Provider { + if (!rootProject.isRoot) { + throw IllegalArgumentException("this should've been the root project") + } + return rootProject.gradle.sharedServices.registerIfAbsent( + SERVICE_NAME, + AffectedModuleDetectorLoader::class.java, + configureAction, + ) + } + + fun getInstance(project: Project): AffectedModuleDetector { + val extensions = project.rootProject.extensions + @Suppress("UNCHECKED_CAST") + val detector = extensions.findByName(ROOT_PROP_NAME) as? AffectedModuleDetector + return detector!! + } + + /** + * Call this method to configure the given task to execute only if the owner project is + * affected by current changes + */ + @Throws(GradleException::class) + @JvmStatic + fun configureTaskGuard(task: Task) { + val detector = getInstance(task.project) + task.onlyIf { detector.shouldInclude(task) } + } + } +} + +/** + * Wrapper for AffectedModuleDetector Callers can access this wrapper during project configuration + * and save it until task execution time when the wrapped detector is ready for use (after the + * project graph is ready) + */ +class AffectedModuleDetectorWrapper : AffectedModuleDetector(logger = null) { + // We save a provider to a build service that knows how to make an + // AffectedModuleDetectorImpl because: + // An AffectedModuleDetectorImpl saves the list of modified files and affected + // modules to avoid having to recompute it for each task. However, that list can + // change across builds and we want to recompute it in each build. This requires + // creating a new AffectedModuleDetectorImpl in each build. + // To get Gradle to create a new AffectedModuleDetectorImpl in each build, we need + // to pass around a provider to a build service and query it from each task. + // The build service gets recreated when absent and reused when present. Then the + // build service will return the same AffectedModuleDetectorImpl for each task in + // a build + var wrapped: Provider? = null + + fun getOrThrow(): AffectedModuleDetector { + return wrapped?.get()?.detector + ?: throw GradleException( + """ + Tried to get the affected module detector implementation too early. + You cannot access it until all projects are evaluated. + """ + .trimIndent() + ) + } + + override fun getSubset(projectPath: String): ProjectSubset { + return getOrThrow().getSubset(projectPath) + } + + override fun shouldInclude(project: String): Boolean { + return getOrThrow().shouldInclude(project) + } + + override fun shouldInclude(task: Task): Boolean { + return getOrThrow().shouldInclude(task) + } +} + +/** + * Stores the parameters of an AffectedModuleDetector and creates one when needed. The parameters + * here may be deserialized and loaded from Gradle's configuration cache when the configuration + * cache is enabled. + */ +abstract class AffectedModuleDetectorLoader : + BuildService { + interface Parameters : BuildServiceParameters { + var acceptAll: Boolean + val enabled: Property + val dependencyMap: MapProperty> + var rootDir: File + var checkoutRoot: File + var projectGraph: ProjectGraph + val logOutputFileProvider: RegularFileProperty + var cobuiltTestPaths: Set>? + var alwaysBuildIfExists: Set? + var ignoredPaths: Set? + var baseCommitOverride: Provider? + var gitChangedFilesProvider: Provider> + } + + val detector: AffectedModuleDetector by lazy { + val file = + parameters.logOutputFileProvider.get().asFile.also { if (it.exists()) it.delete() } + val logger = FileLogger(file) + logger.info("setup: enabled: ${parameters.enabled.get()}") + if (parameters.acceptAll) { + logger.info("using AcceptAll") + AcceptAll(null) + } else { + logger.lifecycle("projects evaluated") + logger.info("using real detector") + val dependencyTracker = + DependencyTracker(parameters.dependencyMap.get(), logger.toLogger()) + AffectedModuleDetectorImpl( + projectGraph = parameters.projectGraph, + dependencyTracker = dependencyTracker, + logger = logger.toLogger(), + cobuiltTestPaths = + parameters.cobuiltTestPaths ?: AffectedModuleDetectorImpl.COBUILT_TEST_PATHS, + alwaysBuildIfExists = + parameters.alwaysBuildIfExists + ?: AffectedModuleDetectorImpl.ALWAYS_BUILD_IF_EXISTS, + ignoredPaths = parameters.ignoredPaths ?: AffectedModuleDetectorImpl.IGNORED_PATHS, + changedFilesProvider = parameters.gitChangedFilesProvider, + ) + } + } +} + +/** Implementation that accepts everything without checking. */ +private class AcceptAll(logger: Logger? = null) : AffectedModuleDetector(logger) { + override fun shouldInclude(project: String): Boolean { + logger?.info("[AcceptAll] acceptAll.shouldInclude returning true") + return true + } + + override fun getSubset(projectPath: String): ProjectSubset { + logger?.info("[AcceptAll] AcceptAll.getSubset returning CHANGED_PROJECTS") + return ProjectSubset.CHANGED_PROJECTS + } +} + +/** + * Real implementation that checks git logs to decide what is affected. + * + * If any file outside a module is changed, we assume everything has changed. + * + * When a file in a module is changed, all modules that depend on it are considered as changed. + */ +class AffectedModuleDetectorImpl( + private val projectGraph: ProjectGraph, + private val dependencyTracker: DependencyTracker, + logger: Logger?, + // used for debugging purposes when we want to ignore non module files + @Suppress("unused") private val ignoreUnknownProjects: Boolean = false, + private val cobuiltTestPaths: Set> = COBUILT_TEST_PATHS, + private val alwaysBuildIfExists: Set = ALWAYS_BUILD_IF_EXISTS, + private val ignoredPaths: Set = IGNORED_PATHS, + private val changedFilesProvider: Provider>, +) : AffectedModuleDetector(logger) { + + private val allProjects by lazy { projectGraph.allProjects } + + val affectedProjects by lazy { changedProjects + dependentProjects } + + val changedProjects by lazy { findChangedProjects() } + + val dependentProjects by lazy { findDependentProjects() } + + val alwaysBuild by lazy { alwaysBuildIfExists.filter { path -> allProjects.contains(path) } } + + private var unknownFiles: MutableSet = mutableSetOf() + + // Files tracked by git that are not expected to effect the build, thus require no consideration + private var ignoredFiles: MutableSet = mutableSetOf() + + val buildAll by lazy { shouldBuildAll() } + + private val cobuiltTestProjects by lazy { lookupProjectSetsFromPaths(cobuiltTestPaths) } + + override fun shouldInclude(project: String): Boolean { + return if (project == ":" || buildAll) { + true + } else { + affectedProjects.contains(project) + } + } + + override fun getSubset(projectPath: String): ProjectSubset { + return when { + changedProjects.contains(projectPath) -> { + ProjectSubset.CHANGED_PROJECTS + } + dependentProjects.contains(projectPath) -> { + ProjectSubset.DEPENDENT_PROJECTS + } + // projects that are only included because of buildAll + else -> { + ProjectSubset.NONE + } + } + } + + /** + * Finds only the set of projects that were directly changed in the commit. This includes + * placeholder-tests and any modules that need to be co-built. + * + * Also populates the unknownFiles var which is used in findAffectedProjects + * + * Returns allProjects if there are no previous merge CLs, which shouldn't happen. + */ + private fun findChangedProjects(): Set { + val changedFiles = changedFilesProvider.getOrNull() ?: return allProjects + + val changedProjects: MutableSet = alwaysBuild.toMutableSet() + + for (filePath in changedFiles) { + if (ignoredPaths.any { filePath.startsWith(it) }) { + ignoredFiles.add(filePath) + logger?.info("Ignoring file: $filePath") + } else { + val containingProject = findContainingProject(filePath) + if (containingProject == null) { + unknownFiles.add(filePath) + logger?.info( + "Couldn't find containing project for file: $filePath. Adding to " + + "unknownFiles." + ) + } else { + changedProjects.add(containingProject) + logger?.info( + "For file $filePath containing project is $containingProject. " + + "Adding to changedProjects." + ) + } + } + } + + return changedProjects + getAffectedCobuiltProjects(changedProjects, cobuiltTestProjects) + } + + /** + * Gets all dependent projects from the set of changedProjects. This doesn't include the + * original changedProjects. Always build is still here to ensure at least 1 thing is built + */ + private fun findDependentProjects(): Set { + val dependentProjects = + changedProjects.flatMap { dependencyTracker.findAllDependents(it) }.toSet() + return dependentProjects + + alwaysBuild + + getAffectedCobuiltProjects(dependentProjects, cobuiltTestProjects) + } + + /** + * Determines whether we are in a state where we want to build all projects, instead of only + * affected ones. This occurs for buildSrc changes, as well as in situations where we determine + * there are no changes within our repository (e.g. prebuilts change only) + */ + private fun shouldBuildAll(): Boolean { + var shouldBuildAll = false + // Should only trigger if there are no changedFiles and no ignored files + if ( + changedProjects.size == alwaysBuild.size && + unknownFiles.isEmpty() && + ignoredFiles.isEmpty() + ) { + shouldBuildAll = true + } else if (unknownFiles.isNotEmpty() && !isGithubInfraChange()) { + shouldBuildAll = true + } + logger?.info( + "unknownFiles: $unknownFiles, changedProjects: $changedProjects, buildAll: " + + "$shouldBuildAll" + ) + + if (shouldBuildAll) { + logger?.info("Building all projects") + if (unknownFiles.isEmpty()) { + logger?.info("because no changed files were detected") + } else { + logger?.info("because one of the unknown files may affect everything in the build") + logger?.info( + """ + The modules detected as affected by changed files are + ${changedProjects + dependentProjects} + """ + .trimIndent() + ) + } + } + return shouldBuildAll + } + + /** + * Returns true if all unknown changed files are contained in github setup related files. + * (.github, playground-common). These files will not affect aosp hence should not invalidate + * changed file tracking (e.g. not cause running all tests) + */ + private fun isGithubInfraChange(): Boolean { + return unknownFiles.all { it.contains(".github") || it.contains("playground-common") } + } + + private fun lookupProjectSetsFromPaths(allSets: Set>): Set> { + return allSets + .map { setPaths -> + var setExists = false + val projectSet = HashSet() + for (path in setPaths) { + if (!allProjects.contains(path)) { + if (setExists) { + throw IllegalStateException( + "One of the projects in the group of projects that are required " + + "to be built together is missing. Looked for " + + setPaths + ) + } + } else { + setExists = true + projectSet.add(path) + } + } + return@map projectSet + } + .toSet() + } + + private fun getAffectedCobuiltProjects( + affectedProjects: Set, + allCobuiltSets: Set>, + ): Set { + val cobuilts = mutableSetOf() + affectedProjects.forEach { project -> + allCobuiltSets.forEach { cobuiltSet -> + if (cobuiltSet.any { project == it }) { + cobuilts.addAll(cobuiltSet) + } + } + } + return cobuilts + } + + private fun findContainingProject(filePath: String): String? { + return projectGraph.findContainingProject(filePath, logger).also { + logger?.info("search result for $filePath resulted in $it") + } + } + + companion object { + // Project paths that we always build if they exist + val ALWAYS_BUILD_IF_EXISTS = + setOf( + // placeholder test project to ensure no failure due to no instrumentation. + // We can eventually remove if we resolve b/127819369 + ":placeholder-tests" + ) + + // Some tests are codependent even if their modules are not. Enable manual bundling of tests + val COBUILT_TEST_PATHS = + setOf( + // Link material and material-ripple + setOf(":compose:material:material-ripple", ":compose:material:material"), + setOf( + ":benchmark:benchmark-macro", + ":benchmark:integration-tests:macrobenchmark-target", + ), // link benchmark-macro's correctness test and its target + setOf( + ":benchmark:benchmark-macro-junit4", + ":benchmark:integration-tests:macrobenchmark-target", + ), // link benchmark-macro-junit4's correctness test and its target + setOf( + ":profileinstaller:integration-tests:profile-verification", + ":profileinstaller:integration-tests:profile-verification-sample", + ":profileinstaller:integration-tests:" + + "profile-verification-sample-no-initializer", + ":benchmark:integration-tests:baselineprofile-consumer", + ), + ) + + val IGNORED_PATHS = + setOf( + "docs/", + "development/", + "playground-common/", + ".github/", + // since we only used AMD for device tests, versions do not affect test outcomes. + "libraryversions.toml", + ) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/BuildPropParser.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/BuildPropParser.kt new file mode 100644 index 0000000000000..35cefa3f5a43c --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/BuildPropParser.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dependencyTracker + +import java.io.File +import org.gradle.api.logging.Logger + +/** + * Utility class that can parse build.prop files and extract the sha's for frameworks/support. + * + * Currently, we don't use it since build system does not give us the right shas. + */ +object BuildPropParser { + /** + * Returns the sha which is the reference sha that we should use to find changed files. + * + * It returns null if an appropriate sha couldn't be found. (e.g. if more than 1 project changed + * or frameworks/support didn't change) + * + * @param appliedPropsFile The applied.props file that is usually located in the out folder. It + * contains information about the build specific SHAs for this build for each module + * @param repoPropsFile The repo.props file that is usually located in the out folder. It + * contains the origin versions for each repository + */ + fun getShaForThisBuild( + appliedPropsFile: File, + repoPropsFile: File, + logger: Logger? = null, + ): BuildRange? { + if (!appliedPropsFile.canRead()) { + logger?.error("cannot read applied props file from ${appliedPropsFile.absolutePath}") + return null + } + if (!repoPropsFile.canRead()) { + logger?.error("cannot read repo props file from ${repoPropsFile.absolutePath}") + return null + } + val appliedProps = appliedPropsFile.readLines(Charsets.UTF_8).filterNot { it.isEmpty() } + if (appliedProps.isEmpty() && appliedProps.size > 2) { + logger?.info( + """ + We'll run everything because seems like too many things changed or nothing is + changed. Changed projects: $appliedProps + """ + .trimIndent() + ) + return null + } + val changedProject = appliedProps[0] + if (changedProject.indexOf("frameworks/support") == -1) { + logger?.info( + """ + Changed project is not frameworks/support. I'll run everything. + Changed project: $changedProject + """ + .trimIndent() + ) + return null + } + val changeSha = changedProject.split(" ").last() + // now find it in repo props + val androidXLineInRepo = + repoPropsFile.readLines(Charsets.UTF_8).firstOrNull { + it.indexOf("frameworks/support") >= 0 + } + if (androidXLineInRepo == null) { + logger?.info("Cannot find the androidX sha in repo props. $repoPropsFile") + return null + } + val repoSha = androidXLineInRepo.split(" ").last() + logger?.info("repo sha: $repoSha change sha: $changeSha") + return BuildRange(buildSha = changeSha, repoSha = repoSha) + } + + data class BuildRange(val repoSha: String, val buildSha: String) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/DependencyTracker.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/DependencyTracker.kt new file mode 100644 index 0000000000000..b95eb97f50563 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/DependencyTracker.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dependencyTracker + +import java.io.Serializable +import org.gradle.api.logging.Logger + +/** + * Utility class that traverses all project dependencies and discover which modules depend on each + * other. This is mainly used by [AffectedModuleDetector] to find out which projects should be run. + * + * @param dependentList A map from a project to the list of projects that depend on it. e.g. if + * project A depends on B, it is stored as B -> {A}. + */ +class DependencyTracker(private val dependentList: Map>, logger: Logger?) : + Serializable { + init { + val stringBuilder = StringBuilder() + dependentList.forEach { (project, dependents) -> + dependents.forEach { dependent -> + stringBuilder.append("there is a dependency from $dependent to $project\n") + } + } + logger?.info(stringBuilder.toString()) + } + + fun findAllDependents(projectPath: String): Set { + val result = mutableSetOf() + fun addAllDependents(projectPath: String) { + if (result.add(projectPath)) { + dependentList[projectPath]?.forEach(::addAllDependents) + } + } + addAllDependents(projectPath) + // the projectPath isn't a dependent of itself + return result.minus(projectPath) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/FileLogger.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/FileLogger.kt new file mode 100644 index 0000000000000..42f2bf6e992a5 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/FileLogger.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dependencyTracker + +import java.io.File +import java.io.Serializable +import org.gradle.api.logging.LogLevel +import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLogger +import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext +import org.gradle.internal.time.Clock + +/** Gradle logger that logs to a file */ +class FileLogger(val file: File) : Serializable { + @Transient var impl: OutputEventListenerBackedLogger? = null + + fun toLogger(): OutputEventListenerBackedLogger { + if (impl == null) { + impl = + OutputEventListenerBackedLogger( + "my_logger", + OutputEventListenerBackedLoggerContext(Clock { System.currentTimeMillis() }) + .also { + it.level = LogLevel.DEBUG + it.setOutputEventListener { file.appendText(it.toString() + "\n") } + }, + Clock { System.currentTimeMillis() }, + ) + } + return impl!! + } + + fun lifecycle(text: String) { + toLogger().lifecycle(text) + } + + fun info(text: String) { + toLogger().info(text) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/ProjectGraph.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/ProjectGraph.kt new file mode 100644 index 0000000000000..469d7171ccf74 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/ProjectGraph.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dependencyTracker + +import androidx.build.getSupportRootFolder +import java.io.File +import java.io.Serializable +import org.gradle.api.Project +import org.gradle.api.logging.Logger + +/** Creates a project graph for fast lookup by file path */ +class ProjectGraph(project: Project, logger: Logger? = null) : Serializable { + private val rootNode: Node + + init { + // always use cannonical file: b/112205561 + logger?.info("initializing ProjectGraph") + rootNode = Node() + val rootProjectDir = project.getSupportRootFolder().canonicalFile + val projects = + if (rootProjectDir == project.rootDir.canonicalFile) { + project.subprojects + } else { + // include root project if it is not the main AndroidX project. + project.subprojects + project + } + projects.forEach { + logger?.info("creating node for ${it.path}") + val relativePath = it.projectDir.canonicalFile.toRelativeString(rootProjectDir) + val sections = relativePath.split(File.separatorChar) + logger?.info("relative path: $relativePath , sections: $sections") + val leaf = sections.fold(rootNode) { left, right -> left.getOrCreateNode(right) } + leaf.projectPath = it.path + } + logger?.info("finished creating ProjectGraph") + } + + /** + * Finds the project that contains the given file. The file's path prefix should match the + * project's path. + */ + fun findContainingProject(filePath: String, logger: Logger? = null): String? { + val sections = filePath.split(File.separatorChar) + logger?.info("finding containing project for $filePath , sections: $sections") + return rootNode.find(sections, 0, logger) + } + + val allProjects by lazy { + val result = mutableSetOf() + rootNode.addAllProjectPaths(result) + result + } + + private class Node() : Serializable { + var projectPath: String? = null + private val children = mutableMapOf() + + fun getOrCreateNode(key: String): Node { + return children.getOrPut(key) { Node() } + } + + fun find(sections: List, index: Int, logger: Logger?): String? { + if (sections.size <= index) { + logger?.info("nothing") + return projectPath + } + val child = children[sections[index]] + return if (child == null) { + logger?.info("no child found, returning ${projectPath ?: "root"}") + projectPath + } else { + child.find(sections, index + 1, logger) + } + } + + fun addAllProjectPaths(collection: MutableSet) { + projectPath?.let { path -> collection.add(path) } + for (child in children.values) { + child.addAllProjectPaths(collection) + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt new file mode 100644 index 0000000000000..d695377221463 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/ToStringLogger.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dependencyTracker + +import org.gradle.api.logging.LogLevel +import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLogger +import org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext +import org.gradle.internal.time.Clock + +/** Gradle logger that logs to a string. */ +class ToStringLogger(private val stringBuilder: StringBuilder = StringBuilder()) : + OutputEventListenerBackedLogger( + "my_logger", + OutputEventListenerBackedLoggerContext(Clock { System.currentTimeMillis() }).also { + it.level = LogLevel.DEBUG + it.setOutputEventListener { stringBuilder.append(it.toString() + "\n") } + }, + Clock { System.currentTimeMillis() }, + ) { + /** Returns the current log. */ + fun buildString() = stringBuilder.toString() +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyallowlist/DependencyAllowlist.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyallowlist/DependencyAllowlist.kt new file mode 100644 index 0000000000000..7580ca9dd2825 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/dependencyallowlist/DependencyAllowlist.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.dependencyallowlist + +import javax.xml.parsers.DocumentBuilderFactory +import org.w3c.dom.Node +import org.w3c.dom.NodeList + +/** + * @param verificationMetadataXml A string containing the entire content of a file that would live + * at + * [gradle/verification-metadata.xml](https://docs.gradle.org/current/userguide/dependency_verification.html#sub:enabling-verification) + * @return a list of strings that are English descriptions of problems with the dependencies (At + * this point, merely checksum dependency components that do not link to bugs that track asking + * them to be signed) + */ +fun allowlistWarnings(verificationMetadataXml: String): List { + return verificationMetadataComponents(verificationMetadataXml) + .filter { !it.hasValidReason() } + .map { + val componentName = it.attributes.getNamedItem("group").textContent + "Add androidx:reason for unsigned component '$componentName'" + + " (See go/androidx-unsigned-bugs)" + } +} + +/** + * @param verificationMetadataXml see [allowlistWarnings] + * @return a list of [Node]s representing all of the components needing validation in the file. + */ +private fun verificationMetadataComponents(verificationMetadataXml: String): List { + // Throw exception if there is not a single element in the file. + val singleComponentsNode = + DocumentBuilderFactory.newInstance() + .apply { isNamespaceAware = true } + .newDocumentBuilder() + .parse(verificationMetadataXml.byteInputStream()) + .getElementsByTagName("components") + .toList() + .single() + + val componentsChildNodes = singleComponentsNode.childNodes.toList() + return componentsChildNodes.filter { + it.nodeType == Node.ELEMENT_NODE && it.nodeName == "component" + } +} + +private const val ANDROIDX_NAMESPACE_URI = "https://developer.android.com/jetpack/androidx" + +private fun Node.hasValidReason(): Boolean { + val reason = attributes.getNamedItemNS(ANDROIDX_NAMESPACE_URI, "reason") + return reason?.textContent?.containsBug() == true +} + +private fun String.containsBug() = contains("b/") || contains("github.com") && contains("issues") + +private fun NodeList.toList() = (0 until length).map { item(it) } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt new file mode 100644 index 0000000000000..4ce0376562564 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt @@ -0,0 +1,865 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.docs + +import androidx.build.configureTaskTimeouts +import androidx.build.dackka.DackkaTask +import androidx.build.dackka.GenerateMetadataTask +import androidx.build.defaultAndroidConfig +import androidx.build.getAndroidJar +import androidx.build.getCheckoutRoot +import androidx.build.getDistributionDirectory +import androidx.build.getKeystore +import androidx.build.getLibraryClasspath +import androidx.build.getSupportRootFolder +import androidx.build.metalava.versionMetadataUsage +import androidx.build.sources.PROJECT_STRUCTURE_METADATA_FILENAME +import androidx.build.sources.multiplatformUsage +import androidx.build.versionCatalog +import androidx.build.workaroundAndroidXDependencyResolutions +import com.android.build.api.attributes.BuildTypeAttr +import com.android.build.api.dsl.LibraryExtension +import com.android.build.gradle.LibraryPlugin +import com.google.gson.GsonBuilder +import java.io.File +import java.io.FileNotFoundException +import java.time.Duration +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.ComponentMetadataContext +import org.gradle.api.artifacts.ComponentMetadataRule +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.Attribute +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.DocsType +import org.gradle.api.attributes.LibraryElements +import org.gradle.api.attributes.Usage +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.file.FileCollection +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.model.ObjectFactory +import org.gradle.api.plugins.JavaBasePlugin +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.Sync +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.bundling.Zip +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.all +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.register +import org.gradle.work.DisableCachingByDefault + +/** + * Plugin that allows to build documentation for a given set of prebuilt and tip of tree projects. + */ +abstract class AndroidXDocsImplPlugin : Plugin { + lateinit var docsSourcesConfiguration: Configuration + lateinit var multiplatformDocsSourcesConfiguration: Configuration + lateinit var versionMetadataConfiguration: Configuration + lateinit var dependencyClasspath: FileCollection + + @get:Inject abstract val archiveOperations: ArchiveOperations + + override fun apply(project: Project) { + val docsType = project.name.removePrefix("docs-") + project.plugins.configureEach { plugin -> + when (plugin) { + is LibraryPlugin -> { + val libraryExtension = project.extensions.getByType() + libraryExtension.compileSdk = + project.defaultAndroidConfig.latestStableCompileSdk + libraryExtension.buildToolsVersion = + project.defaultAndroidConfig.buildToolsVersion + + // Use a local debug keystore to avoid build server issues. + val debugSigningConfig = libraryExtension.signingConfigs.getByName("debug") + debugSigningConfig.storeFile = project.getKeystore() + libraryExtension.buildTypes.configureEach { buildType -> + // Sign all the builds (including release) with debug key + buildType.signingConfig = debugSigningConfig + } + } + } + } + disableUnneededTasks(project) + createConfigurations(project) + val buildOnServer = + project.tasks.register("buildOnServer") { + requiredFile.set(project.getDistributionDirectory().file("docs-$docsType.zip")) + } + + val unzippedKmpSamplesSourcesDirectory = + project.layout.buildDirectory.dir("unzippedMultiplatformSampleSources") + val unzippedJvmSamplesSourcesDirectory = + project.layout.buildDirectory.dir("unzippedJvmSampleSources") + val unzippedJvmSourcesDirectory = project.layout.buildDirectory.dir("unzippedJvmSources") + val unzippedMultiplatformSourcesDirectory = + project.layout.buildDirectory.dir("unzippedMultiplatformSources") + val mergedProjectMetadata = + project.layout.buildDirectory.file( + "project_metadata/$PROJECT_STRUCTURE_METADATA_FILENAME" + ) + val (unzipJvmSourcesTask, unzipJvmSamplesTask) = + configureUnzipJvmSourcesTasks( + project, + unzippedJvmSourcesDirectory, + unzippedJvmSamplesSourcesDirectory, + docsSourcesConfiguration, + ) + val configureMultiplatformSourcesTask = + configureMultiplatformInputsTasks( + project, + unzippedMultiplatformSourcesDirectory, + unzippedKmpSamplesSourcesDirectory, + multiplatformDocsSourcesConfiguration, + mergedProjectMetadata, + ) + + configureDackka( + project = project, + unzippedJvmSourcesDirectory = unzippedJvmSourcesDirectory, + unzippedMultiplatformSourcesDirectory = unzippedMultiplatformSourcesDirectory, + unzipJvmSourcesTask = unzipJvmSourcesTask, + configureMultiplatformSourcesTask = configureMultiplatformSourcesTask, + unzippedJvmSamplesSources = unzippedJvmSamplesSourcesDirectory, + unzipJvmSamplesTask = unzipJvmSamplesTask, + unzippedKmpSamplesSources = unzippedKmpSamplesSourcesDirectory, + dependencyClasspath = dependencyClasspath, + buildOnServer = buildOnServer, + docsConfiguration = docsSourcesConfiguration, + multiplatformDocsConfiguration = multiplatformDocsSourcesConfiguration, + mergedProjectMetadata = mergedProjectMetadata, + docsType = docsType, + ) + + project.configureTaskTimeouts() + project.workaroundAndroidXDependencyResolutions() + } + + /** + * Creates and configures a task that builds a list of select sources from jars and places them + * in [sourcesDestinationDirectory], partitioning samples into [samplesDestinationDirectory]. + */ + private fun configureUnzipJvmSourcesTasks( + project: Project, + sourcesDestinationDirectory: Provider, + samplesDestinationDirectory: Provider, + docsConfiguration: Configuration, + ): Pair, TaskProvider> { + val pairProvider = + docsConfiguration.incoming + .artifactView {} + .files + .elements + .map { + it.map { it.asFile }.toSortedSet().partition { "samples" !in it.toString() } + } + return project.tasks.register("unzipJvmSources", Sync::class.java) { task -> + // Store archiveOperations into a local variable to prevent access to the plugin + // during the task execution, as that breaks configuration caching. + val localVar = archiveOperations + task.into(sourcesDestinationDirectory) + task.from( + pairProvider + .map { it.first } + .map { + it.map { jar -> + localVar.zipTree(jar).matching { it.exclude("**/META-INF/MANIFEST.MF") } + } + } + ) + // Files with the same path in different source jars of the same library will lead to + // some classes/methods not appearing in the docs. + task.duplicatesStrategy = DuplicatesStrategy.WARN + } to + project.tasks.register("unzipSampleSources", Sync::class.java) { task -> + // Store archiveOperations into a local variable to prevent access to the plugin + // during the task execution, as that breaks configuration caching. + val localVar = archiveOperations + task.into(samplesDestinationDirectory) + task.from( + pairProvider + .map { it.second } + .map { + it.map { jar -> + localVar.zipTree(jar).matching { + it.exclude("**/META-INF/MANIFEST.MF") + } + } + } + ) + // We expect this to happen when multiple libraries use the same sample, e.g. + // paging. + task.duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + } + + /** + * Creates multiple tasks to unzip multiplatform sources and merge their metadata to be used as + * input for Dackka. Returns a single umbrella task which depends on the others. + */ + private fun configureMultiplatformInputsTasks( + project: Project, + unzippedMultiplatformSourcesDirectory: Provider, + unzippedMultiplatformSamplesDirectory: Provider, + multiplatformDocsSourcesConfiguration: Configuration, + mergedProjectMetadata: Provider, + ): TaskProvider { + val tempMultiplatformMetadataDirectory = + project.layout.buildDirectory.dir("tmp/multiplatformMetadataFiles") + // unzip the sources into source folder and metadata files into folders per project + val unzipMultiplatformSources = + project.tasks.register( + "unzipMultiplatformSources", + UnzipMultiplatformSourcesTask::class.java, + ) { + it.inputJars.set(multiplatformDocsSourcesConfiguration.incoming.files) + it.metadataOutput.set(tempMultiplatformMetadataDirectory) + it.sourceOutput.set(unzippedMultiplatformSourcesDirectory) + it.samplesOutput.set(unzippedMultiplatformSamplesDirectory) + } + // merge all the metadata files from the individual project dirs + return project.tasks.register( + "mergeMultiplatformMetadata", + MergeMultiplatformMetadataTask::class.java, + ) { + it.mergedProjectMetadata.set(mergedProjectMetadata) + it.inputDirectory.set(unzipMultiplatformSources.flatMap { it.metadataOutput }) + } + } + + /** + * The following configurations are created to build a list of projects that need to be + * documented and should be used from build.gradle of docs projects for the following: + * - docs(project(":foo:foo") or docs("androidx.foo:foo:1.0.0") for docs sources + * - samples(project(":foo:foo-samples") or samples("androidx.foo:foo-samples:1.0.0") for + * samples sources + * - stubs(project(":foo:foo-stubs")) - stubs needed for a documented library + */ + private fun createConfigurations(project: Project) { + project.dependencies.components.all() + val docsConfiguration = + project.configurations.create("docs") { + it.isCanBeResolved = false + it.isCanBeConsumed = false + } + // This exists for libraries that are deprecated or not hosted in the AndroidX repo + val docsWithoutApiSinceConfiguration = + project.configurations.create("docsWithoutApiSince") { + it.isCanBeResolved = false + it.isCanBeConsumed = false + } + val multiplatformDocsConfiguration = + project.configurations.create("kmpDocs") { + it.isCanBeResolved = false + it.isCanBeConsumed = false + } + val stubsConfiguration = + project.configurations.create("stubs") { + it.isCanBeResolved = false + it.isCanBeConsumed = false + } + + fun Configuration.setResolveSources() { + isTransitive = false + isCanBeConsumed = false + attributes { + it.attribute( + Usage.USAGE_ATTRIBUTE, + project.objects.named(Usage.JAVA_RUNTIME), + ) + it.attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category.DOCUMENTATION), + ) + it.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + project.objects.named(DocsType.SOURCES), + ) + it.attribute( + LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, + project.objects.named(LibraryElements.JAR), + ) + } + } + docsSourcesConfiguration = + project.configurations.create("docs-sources") { + it.setResolveSources() + it.extendsFrom(docsConfiguration, docsWithoutApiSinceConfiguration) + } + multiplatformDocsSourcesConfiguration = + project.configurations.create("multiplatform-docs-sources") { configuration -> + configuration.isTransitive = false + configuration.isCanBeConsumed = false + configuration.attributes { + it.attribute(Usage.USAGE_ATTRIBUTE, project.multiplatformUsage) + it.attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category.DOCUMENTATION), + ) + it.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + project.objects.named(DocsType.SOURCES), + ) + it.attribute( + LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, + project.objects.named(LibraryElements.JAR), + ) + } + configuration.extendsFrom(multiplatformDocsConfiguration) + } + + versionMetadataConfiguration = + project.configurations.create("library-version-metadata") { + it.isTransitive = false + it.isCanBeConsumed = false + + it.attributes.attribute(Usage.USAGE_ATTRIBUTE, project.versionMetadataUsage) + it.attributes.attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category.DOCUMENTATION), + ) + it.attributes.attribute( + Bundling.BUNDLING_ATTRIBUTE, + project.objects.named(Bundling.EXTERNAL), + ) + + it.extendsFrom(docsConfiguration, multiplatformDocsConfiguration) + } + + fun Configuration.setResolveClasspathForUsage(usage: String) { + isCanBeConsumed = false + attributes { + it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(usage)) + it.attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category.LIBRARY), + ) + it.attribute( + BuildTypeAttr.ATTRIBUTE, + project.objects.named("release"), + ) + } + extendsFrom(docsConfiguration, stubsConfiguration, docsWithoutApiSinceConfiguration) + } + + // Build a compile & runtime classpaths for needed for documenting the libraries + // from the configurations above. + val docsCompileClasspath = + project.configurations.create("docs-compile-classpath") { + it.setResolveClasspathForUsage(Usage.JAVA_API) + } + val docsRuntimeClasspath = + project.configurations.create("docs-runtime-classpath") { + it.setResolveClasspathForUsage(Usage.JAVA_RUNTIME) + } + val kotlinDefaultCatalogVersion = androidx.build.KotlinTarget.LATEST.catalogVersion + val kotlinLatest = project.versionCatalog.findVersion(kotlinDefaultCatalogVersion).get() + listOf(docsCompileClasspath, docsRuntimeClasspath).forEach { config -> + config.resolutionStrategy { + it.eachDependency { details -> + if (details.requested.group == "org.jetbrains.kotlin") { + details.useVersion(kotlinLatest.requiredVersion) + } + } + } + } + dependencyClasspath = + docsCompileClasspath.incoming + .artifactView { + it.attributes.attribute( + Attribute.of("artifactType", String::class.java), + "android-classes", + ) + } + .files + + docsRuntimeClasspath.incoming + .artifactView { + it.attributes.attribute( + Attribute.of("artifactType", String::class.java), + "android-classes", + ) + } + .files + } + + private fun configureDackka( + project: Project, + unzippedJvmSourcesDirectory: Provider, + unzippedMultiplatformSourcesDirectory: Provider, + unzipJvmSourcesTask: TaskProvider, + configureMultiplatformSourcesTask: TaskProvider, + unzippedJvmSamplesSources: Provider, + unzipJvmSamplesTask: TaskProvider, + unzippedKmpSamplesSources: Provider, + dependencyClasspath: FileCollection, + buildOnServer: TaskProvider<*>, + docsConfiguration: Configuration, + multiplatformDocsConfiguration: Configuration, + mergedProjectMetadata: Provider, + docsType: String, + ) { + val generatedDocsDir = project.layout.buildDirectory.dir("docs") + val generateMetadataTask = + project.tasks.register("generateMetadata", GenerateMetadataTask::class.java) { task -> + val artifacts = docsConfiguration.incoming.artifacts.resolvedArtifacts + task.getArtifactIds().set(artifacts.map { result -> result.map { it.id } }) + task.getArtifactFiles().set(artifacts.map { result -> result.map { it.file } }) + val multiplatformArtifacts = + multiplatformDocsConfiguration.incoming.artifacts.resolvedArtifacts + task + .getMultiplatformArtifactIds() + .set(multiplatformArtifacts.map { result -> result.map { it.id } }) + task + .getMultiplatformArtifactFiles() + .set(multiplatformArtifacts.map { result -> result.map { it.file } }) + task.destinationFile.set(getMetadataRegularFile(project)) + } + + val metricsFile = project.layout.buildDirectory.file("build-metrics.json") + val projectName = project.name + + val dackkaTask = + project.tasks.register("docs", DackkaTask::class.java) { task -> + var taskStartTime: LocalDateTime? = null + task.argsJsonFile.set( + project.getDistributionDirectory().file("dackkaArgs-${project.name}.json") + ) + task.apply { + // Remove once there is property version of Copy#destinationDir + // Use samplesDir.set(unzipSamplesTask.flatMap { it.destinationDirectory }) + // https://github.com/gradle/gradle/issues/25824 + dependsOn(unzipJvmSourcesTask) + dependsOn(unzipJvmSamplesTask) + dependsOn(configureMultiplatformSourcesTask) + + description = + "Generates reference documentation using a Google devsite Dokka" + + " plugin. Places docs in ${generatedDocsDir.get()}" + group = JavaBasePlugin.DOCUMENTATION_GROUP + + dackkaClasspath.from(project.getLibraryClasspath("dackka")) + destinationDir.set(generatedDocsDir) + frameworkSamplesDir.set(File(project.getSupportRootFolder(), "samples")) + samplesJvmDir.set(unzippedJvmSamplesSources) + samplesKmpDir.set(unzippedKmpSamplesSources) + jvmSourcesDir.set(unzippedJvmSourcesDirectory) + multiplatformSourcesDir.set(unzippedMultiplatformSourcesDirectory) + projectListsDirectory.set( + File(project.getSupportRootFolder(), "docs-public/package-lists") + ) + dependenciesClasspath.from( + dependencyClasspath + + project.getAndroidJar( + project.defaultAndroidConfig.latestStableCompileSdk + ) + + project.getExtraCommonDependencies() + ) + excludedPackages.set(hiddenPackages.toSet()) + excludedPackagesForJava.set(hiddenPackagesJava) + excludedPackagesForKotlin.set(emptySet()) + libraryMetadataFile.set(generateMetadataTask.flatMap { it.destinationFile }) + projectStructureMetadataFile.set(mergedProjectMetadata) + // See go/dackka-source-link for details on these links. + baseSourceLink.set("https://cs.android.com/search?q=file:%s+class:%s") + baseFunctionSourceLink.set( + "https://cs.android.com/search?q=file:%s+function:%s" + ) + basePropertySourceLink.set("https://cs.android.com/search?q=file:%s+symbol:%s") + annotationsNotToDisplay.set(hiddenAnnotations) + annotationsNotToDisplayJava.set(hiddenAnnotationsJava) + annotationsNotToDisplayKotlin.set(hiddenAnnotationsKotlin) + hidingAnnotations.set(annotationsToHideApis) + nullabilityAnnotations.set(validNullabilityAnnotations) + versionMetadataFiles.from(versionMetadataConfiguration.incoming.files) + task.doFirst { taskStartTime = LocalDateTime.now() } + task.doLast { + val cpus = + try { + ProcessBuilder("lscpu") + .start() + .apply { waitFor(100L, TimeUnit.MILLISECONDS) } + .inputStream + .bufferedReader() + .readLines() + .filter { it.startsWith("CPU(s):") } + .singleOrNull() + ?.split(" ") + ?.last() + ?.toInt() + } catch (e: java.io.IOException) { + null + } // not running on linux + if (cpus != 64) { // Keep stddev of build metrics low b/334867245 + println("$cpus cpus, so not storing build metrics.") + return@doLast + } + println("$cpus cpus, so storing build metrics.") + val taskEndTime = LocalDateTime.now() + val duration = Duration.between(taskStartTime, taskEndTime).toMillis() + metricsFile + .get() + .asFile + .writeText("{ \"${projectName}_docs_execution_duration\": $duration }") + } + } + } + + val zipTask = + project.tasks.register("zipDocs", Zip::class.java) { task -> + task.apply { + from(dackkaTask.flatMap { it.destinationDir }) + + val baseName = "docs-$docsType" + archiveBaseName.set(baseName) + destinationDirectory.set(project.getDistributionDirectory()) + group = JavaBasePlugin.DOCUMENTATION_GROUP + } + } + buildOnServer.configure { it.dependsOn(zipTask) } + } + + /** + * Replace all tests etc with empty task, so we don't run anything it is more effective then + * task.enabled = false, because we avoid executing deps as well + */ + private fun disableUnneededTasks(project: Project) { + var reentrance = false + project.tasks.whenTaskAdded { task -> + if ( + task is Test || + task.name.startsWith("assemble") || + task.name == "lint" || + task.name == "lintDebug" || + task.name == "lintAnalyzeDebug" || + task.name == "transformDexArchiveWithExternalLibsDexMergerForPublicDebug" || + task.name == "transformResourcesWithMergeJavaResForPublicDebug" || + task.name == "checkPublicDebugDuplicateClasses" + ) { + if (!reentrance) { + reentrance = true + project.tasks.named(task.name) { + it.actions = emptyList() + it.dependsOn(emptyList()) + } + reentrance = false + } + } + } + } +} + +@DisableCachingByDefault(because = "Doesn't benefit from caching") +abstract class DocsBuildOnServer : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val requiredFile: RegularFileProperty + + @TaskAction + fun checkAllBuildOutputs() { + val file = requiredFile.get().asFile + if (!file.exists()) { + throw FileNotFoundException("buildOnServer required output missing: ${file.path}") + } + } +} + +/** + * Adapter rule to handles prebuilt dependencies that do not use Gradle Metadata (only pom). We + * create a new variant sources that we can later use in the same way we do for tip of tree projects + * and prebuilts with Gradle Metadata. + */ +abstract class SourcesVariantRule : ComponentMetadataRule { + @get:Inject abstract val objects: ObjectFactory + + override fun execute(context: ComponentMetadataContext) { + context.details.maybeAddVariant("sources", "runtime") { + it.attributes { + it.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + it.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION)) + it.attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType.SOURCES)) + } + it.withFiles { + it.removeAllFiles() + it.addFile("${context.details.id.name}-${context.details.id.version}-sources.jar") + } + } + } +} + +/** + * Location of the library metadata JSON file that's used by Dackka, represented as a [RegularFile] + */ +private fun getMetadataRegularFile(project: Project): Provider = + project.layout.buildDirectory.file("AndroidXLibraryMetadata.json") + +// List of packages to exclude from both Java and Kotlin refdoc generation +private val hiddenPackages = + listOf( + "androidx.camera.camera2.impl", + "androidx.camera.camera2.internal.*", + "androidx.camera.core.impl.*", + "androidx.camera.core.internal.*", + "androidx.core.internal", + "androidx.preference.internal", + "androidx.wear.internal.widget.drawer", + "androidx.webkit.internal", + "androidx.work.impl.*", + ) + +// Set of packages to exclude from Java refdoc generation +private val hiddenPackagesJava = + setOf("androidx.*compose.*", "androidx.*glance.*", "androidx\\.tv\\..*") + +// List of annotations which should not be displayed in the docs +private val hiddenAnnotations: List = + listOf( + // This information is compose runtime implementation details; not useful for most, those + // who + // would want it should look at source + "androidx.compose.runtime.Stable", + "androidx.compose.runtime.Immutable", + "androidx.compose.runtime.ReadOnlyComposable", + // This opt-in requirement is non-propagating so developers don't need to know about it + // https://kotlinlang.org/docs/opt-in-requirements.html#non-propagating-opt-in + "androidx.annotation.OptIn", + "kotlin.OptIn", + // This annotation is used mostly in paging, and was removed at the request of the paging + // team + "androidx.annotation.CheckResult", + // This annotation is generated upstream. Dokka uses it for signature serialization. It + // doesn't + // seem useful for developers + "kotlin.ParameterName", + // This annotations is not useful for developers but right now is @ShowAnnotation? + "kotlin.js.JsName", + // This annotation is intended to target the compiler and is general not useful for devs. + "java.lang.Override", + // This annotation is used by the room processor and isn't useful for developers + "androidx.room3.Ignore", + // This is an internal annotation only used by the kotlin compiler. + "kotlin.ExtensionFunctionType", + ) + +val validNullabilityAnnotations = + listOf( + "org.jspecify.annotations.NonNull", + "org.jspecify.annotations.Nullable", + "androidx.annotation.Nullable", + "android.annotation.Nullable", + "androidx.annotation.NonNull", + "android.annotation.NonNull", + // Required by media3 + "org.checkerframework.checker.nullness.qual.Nullable", + ) + +// Annotations which should not be displayed in the Kotlin docs, in addition to hiddenAnnotations +private val hiddenAnnotationsKotlin: List = emptyList() + +// Annotations which should not be displayed in the Java docs, in addition to hiddenAnnotations +private val hiddenAnnotationsJava: List = emptyList() + +// Annotations which mean the elements they are applied to should be hidden from the docs +private val annotationsToHideApis: List = + listOf( + "androidx.annotation.RestrictTo", + // Appears in androidx.test sources + "dagger.internal.DaggerGenerated", + ) + +/** Data class that matches JSON structure of kotlin source set metadata */ +data class ProjectStructureMetadata(var sourceSets: List) + +data class SourceSetMetadata( + val name: String, + val analysisPlatform: String, + var dependencies: List, +) + +@CacheableTask +abstract class UnzipMultiplatformSourcesTask() : DefaultTask() { + + @get:Classpath abstract val inputJars: Property + + @get:OutputDirectory abstract val metadataOutput: DirectoryProperty + + @get:OutputDirectory abstract val sourceOutput: DirectoryProperty + + @get:OutputDirectory abstract val samplesOutput: DirectoryProperty + + @get:Inject abstract val fileSystemOperations: FileSystemOperations + + @get:Inject abstract val archiveOperations: ArchiveOperations + + @TaskAction + fun execute() { + listOf(sourceOutput, samplesOutput).map { it.get().asFile.deleteRecursively() } + val (sources, samples) = + inputJars + .get() + .associate { it.name to archiveOperations.zipTree(it) } + .toSortedMap() + // Now that we publish sample jars, they can get confused with normal source + // jars. We want to handle sample jars separately, so filter by the name. + .partition { name -> "samples" !in name } + + fileSystemOperations.sync { + it.duplicatesStrategy = DuplicatesStrategy.FAIL + it.from(sources.values) + it.into(sourceOutput) + it.exclude("META-INF/*") + // TODO(b/418945918): Remove when the files below are deduped: + // benchmark/benchmark-traceprocessor/src/androidMain/kotlin/perfetto/protos/package-info.java + // tracing/tracing-wire/src/androidMain/kotlin/perfetto/protos/package-info.java + var seenPath = false + it.eachFile { file -> + val relPath = file.relativePath.pathString + if (relPath == "androidMain/perfetto/protos/package-info.java") { + if (seenPath) { + file.exclude() + } + seenPath = true + } + } + } + + fileSystemOperations.sync { + // Some libraries share samples, e.g. paging. This can be an issue if and only if the + // consumer libraries have pinned samples version or are not in an atomic group. + // We don't have anything matching this case now, but should enforce better. b/334825580 + it.duplicatesStrategy = DuplicatesStrategy.INCLUDE + it.from(samples.values) + it.into(samplesOutput) + it.exclude("META-INF/*") + } + sources.forEach { (name, fileTree) -> + fileSystemOperations.sync { + it.from(fileTree) + it.into(metadataOutput.file(name)) + it.include("META-INF/*") + } + } + } +} + +private fun Map.partition(condition: (K) -> Boolean): Pair, Map> = + this.toList().partition { (k, _) -> condition(k) }.let { it.first.toMap() to it.second.toMap() } + +/** Merges multiplatform metadata files created by [CreateMultiplatformMetadata] */ +@CacheableTask +abstract class MergeMultiplatformMetadataTask : DefaultTask() { + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val inputDirectory: DirectoryProperty + @get:OutputFile abstract val mergedProjectMetadata: RegularFileProperty + + @TaskAction + fun execute() { + val mergedMetadata = ProjectStructureMetadata(sourceSets = listOf()) + inputDirectory + .get() + .asFile + .walkTopDown() + .filter { file -> file.name == PROJECT_STRUCTURE_METADATA_FILENAME } + .forEach { metaFile -> + val gson = GsonBuilder().create() + val metadata = + gson.fromJson(metaFile.readText(), ProjectStructureMetadata::class.java) + mergedMetadata.merge(metadata) + } + val gson = GsonBuilder().setPrettyPrinting().create() + // Sort sourceSets to ensure that child sourceSets come after their parents, b/404784813 + // Also ensure deterministic order--mergedMetadata.merge() uses .toSet() to deduplicate. + mergedMetadata.sourceSets = + mergedMetadata.sourceSets.sortedWith(compareBy({ it.dependencies.size }, { it.name })) + val json = gson.toJson(mergedMetadata) + mergedProjectMetadata.get().asFile.apply { + parentFile.mkdirs() + createNewFile() + writeText(json) + } + } + + private fun ProjectStructureMetadata.merge(metadata: ProjectStructureMetadata) { + val originalSourceSets = this.sourceSets + metadata.sourceSets.forEach { newSourceSet -> + val existingSourceSet = originalSourceSets.find { it.name == newSourceSet.name } + if (existingSourceSet != null) { + existingSourceSet.dependencies = + (newSourceSet.dependencies + existingSourceSet.dependencies).toSet().toList() + } else { + sourceSets += listOf(newSourceSet) + } + } + } +} + +private fun Project.getPrebuiltsExternalPath() = + File(project.getCheckoutRoot(), "prebuilts/androidx/external/") + +private val PLATFORMS = + listOf("linuxx64", "macosarm64", "macosx64", "iosx64", "iossimulatorarm64", "iosarm64") + +private fun Project.getExtraCommonDependencies(): FileCollection = + files( + arrayOf( + File( + getPrebuiltsExternalPath(), + "org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4/" + + "kotlinx-coroutines-core-1.6.4.jar", + ), + File( + getPrebuiltsExternalPath(), + "org/jetbrains/kotlinx/atomicfu/0.17.0/atomicfu-0.17.0.jar", + ), + File(getPrebuiltsExternalPath(), "com/squareup/okio/okio-jvm/3.1.0/okio-jvm-3.1.0.jar"), + // TODO(b/409256436): Remove when KMP classes (.knm) in Kotlin 2.1 can be loaded + File( + getPrebuiltsExternalPath(), + "org/jetbrains/kotlin/kotlin-stdlib/2.0.20/kotlin-stdlib-2.0.20-common.jar", + ), + ) + + PLATFORMS.map { + File( + getPrebuiltsExternalPath(), + "com/squareup/okio/okio-$it/3.1.0/okio-$it-3.1.0.klib", + ) + } + ) diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/CheckTipOfTreeDocsTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/CheckTipOfTreeDocsTask.kt new file mode 100644 index 0000000000000..5ea8353bd0342 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/CheckTipOfTreeDocsTask.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.docs + +import androidx.build.AndroidXExtension +import androidx.build.SoftwareType +import androidx.build.addToBuildOnServer +import androidx.build.checkapi.shouldConfigureApiTasks +import androidx.build.getSupportRootFolder +import androidx.build.multiplatformExtension +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** + * Verifies that the text of the [projectPathProvider] can be found in the [tipOfTreeBuildFile] to + * enforce that projects enable docs generation. + */ +@CacheableTask +abstract class CheckTipOfTreeDocsTask : DefaultTask() { + @get:[InputFile PathSensitive(PathSensitivity.NONE)] + abstract val tipOfTreeBuildFile: RegularFileProperty + + @get:Input abstract val projectPathProvider: Property + + @get:Input abstract val type: Property + + @get:Input abstract val requiresDocs: Property + + @TaskAction + fun exec() { + if (!requiresDocs.get()) return + + val projectPath = projectPathProvider.get() + // Make sure not to allow a partial project path match, e.g. ":activity:activity" shouldn't + // match ":activity:activity-ktx", both need to be listed separately. + val projectDependency = "project(\"$projectPath\")" + + val prefix = type.get().prefix + // Check that projects are listed with the right configuration type (docs, kmpDocs, samples) + val fullExpectedText = "$prefix($projectDependency)" + + val fileContents = tipOfTreeBuildFile.asFile.get().readText() + val foundExpectedText = fileContents.contains(fullExpectedText) + + if (!foundExpectedText) { + // If this is a KMP project, check if it is present but configured as non-KMP + val message = + if (fileContents.contains(projectDependency)) { + "Project $projectPath has the wrong configuration type in " + + "docs-tip-of-tree/build.gradle, should use $prefix\n\n" + + "Update the entry for $projectPath in docs-tip-of-tree/build.gradle to " + + "'$fullExpectedText'." + } else { + "Project $projectPath not found in docs-tip-of-tree/build.gradle\n\n" + + "Use the project creation script (development/project-creator/" + + "create_project.py) when setting up a project to make sure all required " + + "steps are complete.\n\n" + + "The project should be added to docs-tip-of-tree/build.gradle as " + + "\'$fullExpectedText\'.\n\n" + + "If this project should not have published refdocs, first check that the " + + "library type listed in its build.gradle file is accurate. If it is, opt out " + + "of refdoc generation using \'doNotDocumentReason = \"some reason\"\' in the " + + "'androidx' configuration section (this is not common)." + } + throw GradleException(message) + } + } + + companion object { + fun Project.setUpCheckDocsTask(extension: AndroidXExtension) { + val docsTypeProvider = + extension.type.map { softwareType -> + if (softwareType == SoftwareType.SAMPLES) { + DocsType.SAMPLES + } else if (multiplatformExtension != null) { + DocsType.KMP + } else { + DocsType.STANDARD + } + } + + val checkDocs = + project.tasks.register("checkDocsTipOfTree", CheckTipOfTreeDocsTask::class.java) { + task -> + task.tipOfTreeBuildFile.set( + project.getSupportRootFolder().resolve("docs-tip-of-tree/build.gradle") + ) + task.projectPathProvider.set(path) + task.type.set(docsTypeProvider) + task.requiresDocs.set(extension.requiresDocs()) + task.cacheEvenIfNoOutputs() + } + project.addToBuildOnServer(checkDocs) + } + + enum class DocsType(val prefix: String) { + STANDARD("docs"), + KMP("kmpDocs"), + SAMPLES("samples"), + } + + /** + * Whether the project should have public docs. True for API-tracked projects and samples, + * unless opted-out with [AndroidXExtension.doNotDocumentReason] + */ + fun AndroidXExtension.requiresDocs() = + shouldConfigureApiTasks().map { it && doNotDocumentReason == null } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS new file mode 100644 index 0000000000000..ef873ccc99491 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS @@ -0,0 +1,3 @@ +asfalcone@google.com +fsladkey@google.com +juliamcclellan@google.com diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/gitclient/ChangeInfo.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/gitclient/ChangeInfo.kt new file mode 100644 index 0000000000000..d0a23c54ad93d --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/gitclient/ChangeInfo.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.gitclient + +import androidx.build.parseXml +import com.google.gson.Gson +import java.io.File +import org.gradle.api.GradleException +import org.gradle.api.provider.Property +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters + +/** + * A provider of changed files based on changeinfo files and manifest files created by the build + * server. + * + * For sample changeinfo config files, see: ChangeInfoProvidersTest.kt + * https://android-build.googleplex.com/builds/pending/P28356101/androidx_incremental/latest/incremental/P28356101-changeInfo + * + * For more information, see b/171569941 + */ +internal abstract class NonGitChangedFilesSource : + ValueSource, NonGitChangedFilesSource.Parameters> { + interface Parameters : ValueSourceParameters { + val projectDirRelativeToRoot: Property + val baseCommitOverridePresent: Property + } + + override fun obtain(): List? { + val changeInfo = System.getenv("CHANGE_INFO") + val manifest = System.getenv("MANIFEST") + val hasChangeInfo = changeInfo != null + val hasManifest = manifest != null + return when { + hasChangeInfo && hasManifest -> { + if (parameters.baseCommitOverridePresent.get()) { + throw GradleException( + "Overriding base commit is not supported when using CHANGE_INFO and MANIFEST" + ) + } + val changeInfoText = File(changeInfo).readText() + val manifestText = File(manifest).readText() + return getChangedFilesFromChangeInfoAndManifest( + changeInfoText, + manifestText, + parameters.projectDirRelativeToRoot.get(), + ) + } + hasChangeInfo xor hasManifest -> { + throw GradleException( + if (hasChangeInfo) "Setting CHANGE_INFO requires also setting MANIFEST" + else "Setting MANIFEST requires also setting CHANGE_INFO" + ) + } + else -> null + } + } +} + +internal fun getChangedFilesFromChangeInfoAndManifest( + changeInfoText: String, + manifestText: String, + projectDirRelativeToRoot: String, +): List { + val fileList = mutableListOf() + val fileSet = mutableSetOf() + val gson = Gson() + val changeInfoEntries = gson.fromJson(changeInfoText, ChangeInfo::class.java) + val projectName = computeProjectName(projectDirRelativeToRoot, manifestText) + val changes = changeInfoEntries.changes?.filter { it.project == projectName } ?: emptyList() + for (change in changes) { + val revisions = change.revisions ?: listOf() + for (revision in revisions) { + val fileInfos = revision.fileInfos ?: listOf() + for (fileInfo in fileInfos) { + fileInfo.oldPath?.let { path -> + if (!fileSet.contains(path)) { + fileList.add(path) + fileSet.add(path) + } + } + fileInfo.path?.let { path -> + if (!fileSet.contains(path)) { + fileList.add(path) + fileSet.add(path) + } + } + } + } + } + return fileList +} + +// Data classes uses to parse CHANGE_INFO json files +internal data class ChangeInfo(val changes: List?) + +internal data class ChangeEntry(val project: String, val revisions: List?) + +internal data class Revisions(val fileInfos: List?) + +internal data class FileInfo(val path: String?, val oldPath: String?, val status: String) + +/** + * A provider of HEAD SHA based on manifest file created by the build server. + * + * For sample manifest files, see: ChangeInfoProvidersTest.kt + * + * For more information, see b/171569941 + */ +internal abstract class NonGitHeadShaSource : ValueSource { + interface Parameters : ValueSourceParameters { + val projectDirRelativeToRoot: Property + } + + override fun obtain(): String? { + val manifest = System.getenv("MANIFEST") ?: return null + return getHeadShaFromManifest( + File(manifest).readText(), + parameters.projectDirRelativeToRoot.get(), + ) + } +} + +internal fun getHeadShaFromManifest( + manifestText: String, + projectDirRelativeToRoot: String, +): String { + val projectName = computeProjectName(projectDirRelativeToRoot, manifestText) + val revisionRegex = Regex("revision=\"([^\"]*)\"") + for (line in manifestText.split("\n")) { + if (line.contains("name=\"${projectName}\"")) { + val result = revisionRegex.find(line)?.groupValues?.get(1) + if (result != null) { + return result + } + } + } + throw GradleException("Could not identify version of project '$projectName' from config text") +} + +private fun computeProjectName(projectPath: String, config: String): String { + fun pathContains(ancestor: String, child: String): Boolean { + return "$child/".startsWith("$ancestor/") + } + val document = parseXml(config, mapOf()) + val projectIterator = document.rootElement.elementIterator() + while (projectIterator.hasNext()) { + val project = projectIterator.next() + val repositoryPath = project.attributeValue("path") + if (repositoryPath != null) { + if (pathContains(repositoryPath, projectPath)) { + val name = project.attributeValue("name") + check(name != null) { "Could not get name for project $project" } + return name + } + } + } + throw GradleException("Could not find project with path '$projectPath' in config") +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt new file mode 100644 index 0000000000000..a877002732e44 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.gitclient + +import androidx.build.getCheckoutRoot +import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.charset.Charset +import javax.inject.Inject +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters +import org.gradle.process.ExecOperations + +/** + * @param baseCommitOverride optional value to use to override last merge commit + * @return provider that has the changes files since the last merge commit. It will use CHANGE_INFO + * and MANIFEST to resolve the files if these environmental variables are set, otherwise it will + * default to using git. + */ +fun Project.getChangedFilesProvider(baseCommitOverride: Provider): Provider> { + return providers + .of(NonGitChangedFilesSource::class.java) { + it.parameters.projectDirRelativeToRoot.set( + projectDir.relativeTo(getCheckoutRoot()).toString() + ) + it.parameters.baseCommitOverridePresent.set( + baseCommitOverride.map { true }.orElse(false) + ) + } + .orElse( + providers.of(GitChangedFilesSource::class.java) { + it.parameters.workingDir.set(rootProject.layout.projectDirectory) + it.parameters.baseCommitOverride.set(baseCommitOverride) + } + ) +} + +/** + * @return provider of HEAD SHA. It will use MANIFEST to get the SHA if the environmental variable + * is set, otherwise it will default to using git. + */ +fun Project.getHeadShaProvider(): Provider { + return providers + .of(NonGitHeadShaSource::class.java) { + it.parameters.projectDirRelativeToRoot.set( + projectDir.relativeTo(getCheckoutRoot()).toString() + ) + } + .orElse( + providers.of(GitHeadShaSource::class.java) { + it.parameters.workingDir.set(project.layout.projectDirectory) + } + ) +} + +/** Provides HEAD SHA by calling git in [Parameters.workingDir]. */ +internal abstract class GitHeadShaSource : ValueSource { + interface Parameters : ValueSourceParameters { + val workingDir: DirectoryProperty + } + + @get:Inject abstract val execOperations: ExecOperations + + override fun obtain(): String { + val output = ByteArrayOutputStream() + execOperations.exec { + it.commandLine("git", "rev-parse", "HEAD") + it.standardOutput = output + it.workingDir = findGitDirInParentFilepath(parameters.workingDir.get().asFile) + } + return String(output.toByteArray(), Charset.defaultCharset()).trim() + } +} + +/** Provides changed files since the last merge by calling git in [Parameters.workingDir]. */ +internal abstract class GitChangedFilesSource : + ValueSource, GitChangedFilesSource.Parameters> { + interface Parameters : ValueSourceParameters { + val workingDir: DirectoryProperty + val baseCommitOverride: Property + } + + @get:Inject abstract val execOperations: ExecOperations + + override fun obtain(): List { + val output = ByteArrayOutputStream() + val gitDirInParentFilepath = findGitDirInParentFilepath(parameters.workingDir.get().asFile) + val baseCommit = + if (parameters.baseCommitOverride.isPresent) { + parameters.baseCommitOverride.get() + } else { + // Call git to get the last merge commit + execOperations.exec { + it.commandLine( + "git", + "log", + "-1", + "--merges", + "--oneline", + "--pretty=format:%H", + ) + it.standardOutput = output + it.workingDir = gitDirInParentFilepath + } + String(output.toByteArray(), Charset.defaultCharset()).trim() + } + output.reset() + // Get the list of changed files since the last git merge commit + execOperations.exec { + it.commandLine("git", "diff", "--name-only", "HEAD", baseCommit) + it.standardOutput = output + it.workingDir = gitDirInParentFilepath + } + return String(output.toByteArray(), Charset.defaultCharset()) + .split(System.lineSeparator()) + .filterNot { it.isEmpty() } + } +} + +/** Finds the git directory containing the given File by checking parent directories */ +private fun findGitDirInParentFilepath(filepath: File): File? { + var curDirectory: File = filepath + while (curDirectory.path != "/") { + if (File("$curDirectory/.git").exists()) { + return curDirectory + } + curDirectory = curDirectory.parentFile + } + return null +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt new file mode 100644 index 0000000000000..ce6df4ae264c6 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateJavaKzipTask.kt @@ -0,0 +1,181 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.build.kythe + +import androidx.build.checkapi.CompilationInputs +import androidx.build.getCheckoutRoot +import androidx.build.getPrebuiltsRoot +import java.io.File +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.process.ExecOperations + +/** Generates kzip files that are used to index the Java source code in Kythe. */ +@CacheableTask +abstract class GenerateJavaKzipTask +@Inject +constructor(private val execOperations: ExecOperations) : DefaultTask() { + + /** Must be run in the checkout root so as to be free of relative markers */ + @get:Internal val checkoutRoot: File = project.getCheckoutRoot() + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val javaExtractorJar: RegularFileProperty + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourcePaths: ConfigurableFileCollection + + @get:Input abstract val javacCompilerArgs: ListProperty + + /** Path to `vnames.json` file, used for name mappings within Kythe. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val vnamesJson: RegularFileProperty + + @get:Classpath abstract val dependencyClasspath: ConfigurableFileCollection + + @get:Classpath abstract val compiledSources: ConfigurableFileCollection + + @get:Classpath abstract val annotationProcessor: ConfigurableFileCollection + + @get:OutputFile abstract val kzipOutputFile: RegularFileProperty + + @get:OutputDirectory abstract val kytheBuildDirectory: DirectoryProperty + + @TaskAction + fun exec() { + val sourceFiles = + sourcePaths.asFileTree.files + .filter { it.extension == "java" } + .map { it.relativeTo(checkoutRoot) } + + if (sourceFiles.isEmpty()) { + return + } + + val dependencyClasspath = + dependencyClasspath + .filter { it.extension == "jar" } + .let { filteredClasspath -> + if (sourcePaths.asFileTree.files.any { it.extension == "kt" }) { + filteredClasspath + compiledSources + } else { + filteredClasspath + } + } + + val kytheBuildDirectory = kytheBuildDirectory.get().asFile.apply { mkdirs() } + + execOperations.javaexec { + it.mainClass.set("-jar") + it.args(javaExtractorJar.get().asFile) + it.args("--class-path", dependencyClasspath.joinToString(":")) + it.args("--processor-path", annotationProcessor.joinToString(":")) + it.args(javacCompilerArgs.get()) + it.args("-d", kytheBuildDirectory) + it.args(sourceFiles) + it.jvmArgs( + // Without all these flags, the extractor fails to run. Copied from: + // https://github.com/kythe/kythe/blob/v0.0.67/kythe/release/release.BUILD#L99-L106 + "--add-opens=java.base/java.nio=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + "--add-exports=jdk.internal.opt/jdk.internal.opt=ALL-UNNAMED", + ) + it.environment("KYTHE_CORPUS", ANDROIDX_CORPUS) + it.environment("KYTHE_KZIP_ENCODING", "proto") + it.environment( + "KYTHE_OUTPUT_FILE", + kzipOutputFile.get().asFile.relativeTo(checkoutRoot).path, + ) + it.environment("KYTHE_ROOT_DIRECTORY", checkoutRoot.path) + it.environment("KYTHE_VNAMES", vnamesJson.get().asFile.path) + it.workingDir = checkoutRoot + } + } + + internal companion object { + fun setupProject( + project: Project, + compilationInputs: CompilationInputs, + compiledSources: Configuration, + ) { + val annotationProcessorPaths = + project.objects.fileCollection().apply { + project.tasks.withType(JavaCompile::class.java).configureEach { + it.options.annotationProcessorPath?.let { path -> from(path) } + } + } + + val javacCompilerArgs = + project.objects.listProperty(String::class.java).apply { + project.tasks.withType(JavaCompile::class.java).configureEach { + addAll(it.options.compilerArgs) + } + } + + project.tasks.register("generateJavaKzip", GenerateJavaKzipTask::class.java) { task -> + task.apply { + javaExtractorJar.set( + File(project.getPrebuiltsRoot(), "build-tools/common/javac_extractor.jar") + ) + sourcePaths.setFrom(compilationInputs.sourcePaths) + vnamesJson.set(project.getVnamesJson()) + dependencyClasspath.setFrom( + compilationInputs.dependencyClasspath + compilationInputs.bootClasspath + ) + this.compiledSources.setFrom(compiledSources) + kzipOutputFile.set( + project.layout.buildDirectory.file( + "kzips/${project.group}-${project.name}.java.kzip" + ) + ) + kytheBuildDirectory.set(project.layout.buildDirectory.dir("kythe-java-classes")) + annotationProcessor.setFrom(annotationProcessorPaths) + this.javacCompilerArgs.set(javacCompilerArgs) + // Needed so generated files (e.g. protos) are present when generating kzip + // Without this, javac_extractor will throw a compilation error + dependsOn(project.tasks.withType(JavaCompile::class.java)) + } + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt new file mode 100644 index 0000000000000..b9829b5b1dc8c --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/GenerateKotlinKzipTask.kt @@ -0,0 +1,260 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.kythe + +import androidx.build.KotlinTarget +import androidx.build.OperatingSystem +import androidx.build.checkapi.CompilationInputs +import androidx.build.checkapi.MultiplatformCompilationInputs +import androidx.build.getCheckoutRoot +import androidx.build.getOperatingSystem +import androidx.build.getPrebuiltsRoot +import androidx.build.multiplatformExtension +import java.io.File +import java.util.jar.JarOutputStream +import java.util.zip.ZipEntry +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + +/** Generates kzip files that are used to index the Kotlin source code in Kythe. */ +@CacheableTask +abstract class GenerateKotlinKzipTask +@Inject +constructor(private val execOperations: ExecOperations) : DefaultTask() { + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val kotlincExtractorBin: RegularFileProperty + + /** Must be run in the checkout root so as to be free of relative markers */ + @get:Internal val checkoutRoot: File = project.getCheckoutRoot() + + @get:Internal val isKmp: Boolean = project.multiplatformExtension != null + + @get:Input abstract val kotlincFreeCompilerArgs: ListProperty + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourcePaths: ConfigurableFileCollection + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val commonModuleSourcePaths: ConfigurableFileCollection + + /** Path to `vnames.json` file, used for name mappings within Kythe. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + abstract val vnamesJson: RegularFileProperty + + @get:Classpath abstract val dependencyClasspath: ConfigurableFileCollection + + @get:Classpath abstract val compiledSources: ConfigurableFileCollection + + @get:Input abstract val kotlinTarget: Property + + @get:Input abstract val jvmTarget: Property + + @get:OutputFile abstract val kzipOutputFile: RegularFileProperty + + @get:OutputDirectory abstract val kytheClassJarsDir: DirectoryProperty + + @TaskAction + fun exec() { + val sourceFiles = + sourcePaths.asFileTree.files + .takeIf { files -> files.any { it.extension == "kt" } } + ?.filter { it.extension == "kt" || it.extension == "java" } + ?.map { it.relativeTo(checkoutRoot) } + .orEmpty() + + if (sourceFiles.isEmpty()) { + return + } + + val commonSourceFiles = + commonModuleSourcePaths.asFileTree.files + .filter { it.extension == "kt" || it.extension == "java" } + .map { it.relativeTo(checkoutRoot) } + + val dependencyClasspath = + dependencyClasspath.files + .filter { it.exists() } + .mapNotNull { file -> + when { + file.isFile && file.extension == "jar" -> { + file.relativeTo(checkoutRoot) + } + file.isDirectory -> { + file + .createJarFromDirectory( + kytheClassJarsDir.get().asFile, + checkoutRoot, + ) + .relativeTo(checkoutRoot) + } + else -> null + } + } + + val args = buildList { + addAll( + listOf( + // Kythe drops arg[0] as it's unix convention that is the executable name + "kotlinc", + "-jvm-target", + jvmTarget.get().target, + "-no-reflect", + "-no-stdlib", + "-api-version", + kotlinTarget.get().apiVersion.version, + "-language-version", + kotlinTarget.get().apiVersion.version, + "-opt-in=kotlin.contracts.ExperimentalContracts", + ) + ) + } + + val multiplatformArg = + if (isKmp) { + listOf("-Xmulti-platform") + } else emptyList() + + val filteredKotlincFreeCompilerArgs = + kotlincFreeCompilerArgs.get().distinct().filter { !it.startsWith("-Xjdk-release") } + + val command = buildList { + add(kotlincExtractorBin.get().asFile) + addAll( + listOf( + "-corpus", + ANDROIDX_CORPUS, + "-kotlin_out", + compiledSources.singleFile.relativeTo(checkoutRoot).path, + "-o", + kzipOutputFile.get().asFile.relativeTo(checkoutRoot).path, + "-vnames", + vnamesJson.get().asFile.relativeTo(checkoutRoot).path, + "-args", + (args + multiplatformArg + filteredKotlincFreeCompilerArgs).joinToString(" "), + ) + ) + sourceFiles.forEach { addAll(listOf("-srcs", it.path)) } + commonSourceFiles.forEach { addAll(listOf("-common_srcs", it.path)) } + dependencyClasspath.forEach { addAll(listOf("-cp", it.path)) } + } + + execOperations.exec { + it.commandLine(command) + it.workingDir = checkoutRoot + } + } + + internal companion object { + fun setupProject( + project: Project, + compilationInputs: CompilationInputs, + compiledSources: Configuration, + kotlinTarget: Property, + javaVersion: JavaVersion, + ) { + val kotlincFreeCompilerArgs = + project.objects.listProperty(String::class.java).apply { + project.tasks.withType(KotlinCompilationTask::class.java).configureEach { + addAll(it.compilerOptions.freeCompilerArgs) + } + } + project.tasks.register("generateKotlinKzip", GenerateKotlinKzipTask::class.java) { task + -> + task.apply { + kotlincExtractorBin.set( + File( + project.getPrebuiltsRoot(), + "build-tools/${osName()}/bin/kotlinc_extractor", + ) + ) + sourcePaths.setFrom(compilationInputs.sourcePaths) + (compilationInputs as? MultiplatformCompilationInputs) + ?.commonModuleSourcePaths + ?.let { commonModuleSourcePaths.from(it) } + vnamesJson.set(project.getVnamesJson()) + dependencyClasspath.setFrom( + compilationInputs.dependencyClasspath + compilationInputs.bootClasspath + ) + this.compiledSources.setFrom(compiledSources) + this.kotlinTarget.set(kotlinTarget) + jvmTarget.set(JvmTarget.fromTarget(javaVersion.toString())) + kzipOutputFile.set( + File( + project.layout.buildDirectory.get().asFile, + "kzips/${project.group}-${project.name}.kotlin.kzip", + ) + ) + kytheClassJarsDir.set(project.layout.buildDirectory.dir("kythe-class-jars")) + this.kotlincFreeCompilerArgs.set(kotlincFreeCompilerArgs) + } + } + } + } +} + +private fun osName() = + when (getOperatingSystem()) { + OperatingSystem.LINUX -> "linux-x86" + OperatingSystem.MAC -> "darwin-x86" + OperatingSystem.WINDOWS -> error("Kzip generation not supported in Windows") + } + +/* Kythe processes only JARs, so we create JARs from directory content. */ +private fun File.createJarFromDirectory(kytheClassJarsDir: File, baseDir: File): File { + val jarParentDir = File(kytheClassJarsDir, this.relativeTo(baseDir).invariantSeparatorsPath) + jarParentDir.mkdirs() + + val jarFile = File(jarParentDir, "${this.name}.jar") + JarOutputStream(jarFile.outputStream()).use { jarOut -> + this.walkTopDown() + .filter { it.isFile } + .forEach { file -> + val entryName = file.relativeTo(this).invariantSeparatorsPath + jarOut.putNextEntry(ZipEntry(entryName)) + file.inputStream().use { it.copyTo(jarOut) } + jarOut.closeEntry() + } + } + return jarFile +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt new file mode 100644 index 0000000000000..f93919222e4cf --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/kythe/KzipTasks.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.kythe + +import androidx.build.AndroidXExtension +import androidx.build.ProjectLayoutType +import androidx.build.checkapi.ApiTaskConfig +import androidx.build.checkapi.configureCompilationInputsAndManifest +import androidx.build.checkapi.createReleaseApiConfiguration +import androidx.build.getSupportRootFolder +import java.io.File +import org.gradle.api.Project +import org.jetbrains.androidx.build.jetBrainsGetDefaultTargetJavaVersion + +/** Sets up tasks for generating kzip files that are used for generating xref support on website. */ +fun Project.configureProjectForKzipTasks(config: ApiTaskConfig, extension: AndroidXExtension) { + // We use the output of kzip tasks for the Kythe pipeline to generate xrefs in cs.android.com + // This is not supported, nor needed in GitHub + if (ProjectLayoutType.isPlayground(this)) { + return + } + + // TODO(b/379936315): Make these compatible with koltinc/javac that indexer is using + if ( + project.path in + listOf( + // Uses Java 9+ APIs, which are not part of any dependency in the classpath + ":room3:room-compiler-processing", + ":room3:room-compiler-processing-testing", + // KSP generated folders not visible to AGP variant api (b/380363756) + ":room3:room-runtime", + // Depends on the generated output of the proto project + // :wear:protolayout:protolayout-proto + // which we haven't captured for Java Kzip generation. + ":wear:tiles:tiles-proto", + ) + ) { + return + } + + // afterEvaluate required to read extension properties + afterEvaluate { + val (compilationInputs, _) = + configureCompilationInputsAndManifest(config) ?: return@afterEvaluate + val compiledSources = createReleaseApiConfiguration() + + GenerateKotlinKzipTask.setupProject( + project, + compilationInputs, + compiledSources, + extension.kotlinTarget, + jetBrainsGetDefaultTargetJavaVersion(extension.type.get(), project), + ) + + GenerateJavaKzipTask.setupProject(project, compilationInputs, compiledSources) + } +} + +internal const val ANDROIDX_CORPUS = + "android.googlesource.com/platform/frameworks/support//androidx-main" + +internal fun Project.getVnamesJson(): File = + File(project.getSupportRootFolder(), "buildSrc/vnames.json") diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/license/AddLicenses.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/license/AddLicenses.kt new file mode 100644 index 0000000000000..6295e22b4eba3 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/license/AddLicenses.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.license + +import androidx.build.License +import androidx.build.ZipStubAarTask +import androidx.build.androidXExtension +import androidx.build.getSupportRootFolder +import androidx.build.multiplatformExtension +import java.io.File +import java.nio.file.Files +import org.gradle.api.Project +import org.gradle.api.tasks.bundling.Zip +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.withType +import org.jetbrains.androidx.build.JetBrainsPublication +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.tasks.CInteropProcess + +/** Adds license file to published JAR, AAR, and Klib artifacts. */ +internal fun Project.addLicensesToPublishedArtifacts(license: License) { + // Use the fork's actual published group (org.jetbrains.*) for the license META-INF path, not the + // redirect-target androidx group. Otherwise a redirect stub's empty artifact and Google's real + // artifact both carry `META-INF/androidx///LICENSE.txt` at the SAME path and collide in the + // consumer's `mergeJavaResource`/AAR packaging. The license belongs at the publishing coordinate. + val forkGroup = runCatching { + JetBrainsPublication.mavenGroupFor(project.path) + }.getOrNull() + val groupSubdir = (forkGroup ?: androidXExtension.mavenGroup?.group!!).replace('.', '/') + val projectSubdir = File(groupSubdir, project.name) + val licenseFile = licenseUrlToLicenseFile[license.url] + + checkNotNull(licenseFile) { + "The ${license.name} license being added to the project ${project.path} is not approved." + } + + // Remove when Gradle creates API for adding license file and setting its location: + // https://github.com/gradle/gradle/issues/29536 + tasks.withType().configureEach { task -> + task.from(licenseFile) { it.into("META-INF/$projectSubdir") } + } + + // Remove when AGP creates API for adding license file and setting its location: + // https://issuetracker.google.com/337785420 + tasks.withType().configureEach { task -> + if (task.name.startsWith("bundle") && task.name.endsWith("Aar")) { + task.from(licenseFile) { it.into("META-INF/$projectSubdir") } + } + } + + tasks.withType().configureEach { task -> + task.from(licenseFile) { it.into("META-INF/$projectSubdir") } + } + + val kmpSubdir = "/default/licenses/$projectSubdir" + // Remove when KMP creates API for adding license file and setting its location: + // https://youtrack.jetbrains.com/issue/KT-69084 + tasks.withType().configureEach { task -> + task.doLast { + val licenseDir = File(task.outputFileProvider.get(), kmpSubdir).toPath() + Files.createDirectories(licenseDir) + Files.write(licenseDir.resolve("LICENSE.txt"), licenseFile.readBytes()) + } + } + + // Remove when KMP creates API for adding license file and setting its location: + // https://youtrack.jetbrains.com/issue/KT-69084 + multiplatformExtension?.targets?.withType()?.configureEach { target -> + target.compilations.configureEach { compilation -> + val compileTaskOutputFileProvider = + compilation.compileTaskProvider.flatMap { it.outputFile } + + compilation.compileTaskProvider.configure { task -> + task.doLast { + val licenseDir = File(compileTaskOutputFileProvider.get(), kmpSubdir).toPath() + Files.createDirectories(licenseDir) + Files.write(licenseDir.resolve("LICENSE.txt"), licenseFile.readBytes()) + } + } + } + } +} + +private val Project.licenseUrlToLicenseFile: Map + get() { + val allowedLicensesFolder = File(getSupportRootFolder(), "buildSrc/allowedLicenses") + return mapOf( + "http://www.apache.org/licenses/LICENSE-2.0.txt" to + File("$allowedLicensesFolder/Apache-2.0/LICENSE.txt"), + "https://opensource.org/licenses/BSD-3-Clause" to + File("$allowedLicensesFolder/BSD-3-Clause/LICENSE.txt"), + ) + } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/license/ValidateLicensesExistTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/license/ValidateLicensesExistTask.kt new file mode 100644 index 0000000000000..10807289faf24 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/license/ValidateLicensesExistTask.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.license + +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** This task validates that all external dependencies have a license file. */ +@DisableCachingByDefault(because = "I/O heavy operation") +abstract class ValidateLicensesExistTask : DefaultTask() { + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val prebuiltsDirectory: DirectoryProperty + + @get:[InputFile PathSensitive(PathSensitivity.NONE)] + abstract val baseline: RegularFileProperty + + @TaskAction + fun validate() { + val baselineFile = baseline.get().asFile + val baseline = + if (baselineFile.exists()) { + baselineFile.readLines().toSet() + } else setOf() + + val violations = mutableSetOf() + prebuiltsDirectory + .get() + .asFile + .walkTopDown() + .onEnter { !File(it, "LICENSE").exists() && !File(it, "NOTICE").exists() } + .forEach { + if (it.extension == "pom") { + violations.add(it.relativeTo(prebuiltsDirectory.get().asFile).toString()) + } + } + val nonBaselinedViolations = (violations - baseline).sorted() + + if (nonBaselinedViolations.isNotEmpty()) + throw GradleException( + """ + Any external library referenced used by androidx + build must have a LICENSE or NOTICE file next to it in the prebuilts. + The following libraries are missing it: + ${nonBaselinedViolations.joinToString("\n")} + """ + .trimIndent() + ) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/lint/ValidateLintChecks.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/lint/ValidateLintChecks.kt new file mode 100644 index 0000000000000..8fc095bf0bfa2 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/lint/ValidateLintChecks.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.lint + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +@DisableCachingByDefault(because = "simple file listing task") +abstract class ValidateLintChecks : DefaultTask() { + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + abstract val sourceDirectories: ConfigurableFileCollection + + @TaskAction + fun validateRegistryTestExists() { + val projectFiles = sourceDirectories.asFileTree.files + // if the project doesn't define a registry it doesn't make sense to test versions + if (projectFiles.none { it.name.contains("Registry") }) { + return + } + projectFiles.find { it.name == "ApiLintVersionsTest.kt" } + ?: throw GradleException("Lint projects should include ApiLintVersionsTest.kt") + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/logging/logging.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/logging/logging.kt new file mode 100644 index 0000000000000..7e0e3ad20e77f --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/logging/logging.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.logging + +internal const val TERMINAL_RED = "\u001B[31m" +internal const val TERMINAL_RESET = "\u001B[0m" diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiCompatibilityTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiCompatibilityTask.kt new file mode 100644 index 0000000000000..c4455a1e77696 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiCompatibilityTask.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.Version +import androidx.build.logging.TERMINAL_RED +import androidx.build.logging.TERMINAL_RESET +import javax.inject.Inject +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkerExecutor + +/** + * This task validates that the API described in one signature txt file is compatible with the API + * in another. + */ +@CacheableTask +internal abstract class CheckApiCompatibilityTask +@Inject +constructor(workerExecutor: WorkerExecutor) : CompatibilityMetalavaTask(workerExecutor) { + + @TaskAction + fun exec() { + check(bootClasspath.files.isNotEmpty()) { "Android boot classpath not set." } + + // Don't allow *any* API changes if we're comparing against a finalized API surface within + // the same major and minor version, e.g. between 1.1.0-beta01 and 1.1.0-beta02 or 1.1.0 and + // 1.1.1. We'll still allow changes between 1.1.0-alpha05 and 1.1.0-beta01. + val currentVersion = version.get() + val referenceVersion = referenceApi.get().version() + val freezeApis = shouldFreezeApis(referenceVersion, currentVersion) + + checkApiFile(restricted = false, referenceVersion, freezeApis) + + if (restrictedApisExist()) { + checkApiFile(restricted = true, referenceVersion, freezeApis) + } + } + + /** + * Confirms that there are no compatibility errors not already listed in the baseline file. + * + * @param restricted whether this compatibility check is for restricted APIs + * @param referenceVersion the version of the previously released APIs + * @param freezeApis whether APIs are frozen and no changes should be allowed + */ + private fun checkApiFile(restricted: Boolean, referenceVersion: Version?, freezeApis: Boolean) { + val baseline = getBaselineFile(restricted) + val args = buildList { + addAll(getCompatibilityArguments(restricted, freezeApis)) + + add("--error-message:compatibility:released") + if (freezeApis && referenceVersion != null) { + add(createFrozenCompatibilityCheckError(referenceVersion.toString())) + } else { + add(CompatibilityCheckError) + } + + if (baseline.exists()) { + add("--baseline") + add(baseline.toString()) + } + } + runWithArgs(args) + } +} + +fun shouldFreezeApis(referenceVersion: Version?, currentVersion: Version) = + referenceVersion != null && + currentVersion.major == referenceVersion.major && + currentVersion.minor == referenceVersion.minor && + referenceVersion.isFinalApi() + +private const val CompatibilityCheckError = + """ + ${TERMINAL_RED}Your change has API compatibility issues. Fix the code according to the messages above.$TERMINAL_RESET + + If you *intentionally* want to break compatibility, you can suppress it with + ./gradlew ignoreApiChanges && ./gradlew updateApi +""" + +private fun createFrozenCompatibilityCheckError(referenceVersion: String) = + """ + ${TERMINAL_RED}The API surface was finalized in $referenceVersion. Revert the changes noted in the errors above.$TERMINAL_RESET + + If you have obtained permission from Android API Council or Jetpack Working Group to bypass this policy, you can suppress this check with: + ./gradlew ignoreApiChanges && ./gradlew updateApi +""" diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiEquivalenceTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiEquivalenceTask.kt new file mode 100644 index 0000000000000..f08945d09db49 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/CheckApiEquivalenceTask.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.checkapi.ApiLocation +import java.io.File +import java.util.concurrent.TimeUnit +import org.apache.commons.io.FileUtils +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** Compares two API txt files against each other. */ +@DisableCachingByDefault(because = "Doesn't benefit from caching") +abstract class CheckApiEquivalenceTask : DefaultTask() { + /** Api file (in the build dir) to check */ + @get:Input abstract val builtApi: Property + + /** Api file (in source control) to compare against */ + @get:Input abstract val checkedInApis: ListProperty + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + fun getTaskInputs(): List { + val checkedInApiLocations = checkedInApis.get() + val checkedInApiFiles = + checkedInApiLocations.flatMap { checkedInApiLocation -> + listOf(checkedInApiLocation.publicApiFile, checkedInApiLocation.restrictedApiFile) + } + + val builtApiLocation = builtApi.get() + val builtApiFiles = + listOf(builtApiLocation.publicApiFile, builtApiLocation.restrictedApiFile) + + return checkedInApiFiles + builtApiFiles + } + + @TaskAction + fun exec() { + val builtApiLocation = builtApi.get() + for (checkedInApi in checkedInApis.get()) { + checkEqual(checkedInApi.publicApiFile, builtApiLocation.publicApiFile) + checkEqual(checkedInApi.restrictedApiFile, builtApiLocation.restrictedApiFile) + } + } +} + +/** + * Returns the output of running the `diff` command-line tool on files [a] and [b], truncated to + * [maxSummaryLines] lines. + */ +fun summarizeDiff(a: File, b: File, maxSummaryLines: Int = 50): String { + if (!a.exists()) { + return "$a does not exist" + } + if (!b.exists()) { + return "$b does not exist" + } + val process = + ProcessBuilder(listOf("diff", a.toString(), b.toString())) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .start() + process.waitFor(5, TimeUnit.SECONDS) + var diffLines = process.inputStream.bufferedReader().readLines().toMutableList() + if (diffLines.size > maxSummaryLines) { + diffLines = diffLines.subList(0, maxSummaryLines) + diffLines.plusAssign("[long diff was truncated]") + } + return diffLines.joinToString("\n") +} + +internal fun checkEqual(expected: File, actual: File) { + if (!FileUtils.contentEquals(expected, actual)) { + val diff = summarizeDiff(expected, actual) + val message = + """API definition has changed + + Declared definition is $expected + True definition is $actual + + Please run `./gradlew updateApi to confirm these changes are + intentional by updating the API definition. + + Difference between these files: + $diff""" + throw GradleException(message) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiLevels.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiLevels.kt new file mode 100644 index 0000000000000..292ec6240aacd --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiLevels.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.Version +import androidx.build.checkapi.ApiLocation +import androidx.build.registerAsComponentForKmpPublishing +import androidx.build.registerAsComponentForPublishing +import java.io.File +import org.gradle.api.Project +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.Usage +import org.gradle.api.tasks.TaskProvider +import org.gradle.kotlin.dsl.named + +/** + * Returns the API files that should be used to generate the API levels metadata. This will not + * include the current version because the source is used as the current version. + */ +fun getFilesForApiLevels(apiFiles: Collection, currentVersion: Version): List { + // Create a map from known versions of the library to signature files + val versionToFileMap = + apiFiles + .mapNotNull { file -> + // Resource API files are not included + if (ApiLocation.isResourceApiFilename(file.name)) return@mapNotNull null + val version = Version.parseFilenameOrNull(file.name) + if (version != null) { + version to file + } else { + null + } + } + .toMap() + + val filteredVersions = filterVersions(versionToFileMap, currentVersion) + return filteredVersions.map { versionToFileMap.getValue(it) } +} + +/** + * From the full set of versions, generates a sorted list of the versions to use when generating the + * API levels metadata. For previous major-minor version cycles, this only includes the latest + * signature file, because we only want one file per stable release. Does not include any files for + * the current major-minor version cycle. + */ +private fun filterVersions( + versionToFileMap: Map, + currentVersion: Version, +): List { + val filteredVersions = mutableListOf() + var prev: Version? = null + for (version in versionToFileMap.keys.sorted()) { + // Add the previous version in the list only if this version is a different major.minor + // version cycle. + if (prev != null && !sameMajorMinor(prev, version)) { + filteredVersions.add(prev) + } + prev = version + } + // Do not include the current version, as the source is used instead of an API file. + if (prev != null && !sameMajorMinor(prev, currentVersion)) { + filteredVersions.add(prev) + } + + return filteredVersions +} + +private fun sameMajorMinor(v1: Version, v2: Version) = v1.major == v2.major && v1.minor == v2.minor + +/** Usage attribute to specify the version metadata component. */ +internal val Project.versionMetadataUsage: Usage + get() = objects.named("library-version-metadata") + +/** Creates a component for the version metadata JSON and registers it for publishing. */ +internal fun Project.registerVersionMetadataComponent( + generateApiTask: TaskProvider +) { + // This needs to non-eager because we call registerAsComponentForPublishing + // which has an enforced timing when we are allowed to add new artifacts + // https://github.com/gradle/gradle/issues/34570 + configurations.create("libraryVersionMetadata") { configuration -> + configuration.isCanBeResolved = false + + configuration.attributes.attribute(Usage.USAGE_ATTRIBUTE, project.versionMetadataUsage) + configuration.attributes.attribute( + Category.CATEGORY_ATTRIBUTE, + objects.named(Category.DOCUMENTATION), + ) + configuration.attributes.attribute( + Bundling.BUNDLING_ATTRIBUTE, + objects.named(Bundling.EXTERNAL), + ) + + // The generate API task has many output files, only add the version metadata as an artifact + val levelsFile = + generateApiTask.map { task -> + task.apiLocation.map { location -> location.apiLevelsFile } + } + configuration.outgoing.artifact(levelsFile) { it.classifier = "versionMetadata" } + + registerAsComponentForPublishing(configuration) + registerAsComponentForKmpPublishing(configuration) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt new file mode 100644 index 0000000000000..f6ba5f56b0fc4 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.Version +import androidx.build.checkapi.ApiLocation +import java.io.File +import javax.inject.Inject +import org.gradle.api.file.Directory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkerExecutor + +/** + * Generate API signature text files from a set of source files, and an API version history JSON + * file from the previous API signature files. + */ +@CacheableTask +internal abstract class GenerateApiTask @Inject constructor(workerExecutor: WorkerExecutor) : + SourceMetalavaTask(workerExecutor) { + + @get:Input var generateRestrictToLibraryGroupAPIs = true + + /** Collection of text files to which API signatures will be written. */ + @get:Internal // already expressed by getTaskOutputs() + abstract val apiLocation: Property + + @OutputFiles + fun getTaskOutputs(): List { + val prop = apiLocation.get() + return listOf(prop.publicApiFile, prop.restrictedApiFile, prop.apiLevelsFile) + } + + @get:Internal abstract val currentVersion: Property + + /** + * The directory where past API files are stored. Not all files in the directory are used, they + * are filtered in [getPastApiFiles]. + */ + @get:Internal abstract var projectApiDirectory: Directory + + /** An ordered list of the API files to use in generating the API level metadata JSON. */ + @InputFiles + @PathSensitive(PathSensitivity.NONE) + fun getPastApiFiles(): List { + return getFilesForApiLevels(projectApiDirectory.asFileTree.files, currentVersion.get()) + } + + @TaskAction + fun exec() { + check(bootClasspath.files.isNotEmpty()) { "Android boot classpath not set." } + check(sourcePaths.files.isNotEmpty()) { "Source paths not set." } + check(compiledSources.files.isNotEmpty()) { + "Compiled sources " + compiledSources + " is empty!" + } + compiledSources.files.forEach { compiled -> + check(compiled.exists()) { "File " + compiled + " does not exist" } + } + + val levelsArgs = + getGenerateApiLevelsArgs( + projectApiDirectory.asFile, + getPastApiFiles(), + currentVersion.get(), + apiLocation.get().apiLevelsFile, + ) + + generateApi( + metalavaClasspath, + createProjectXmlFile(), + sourcePaths.files, + compiledSources.files.singleOrNull(), + apiLocation.get(), + ApiLintMode.CheckBaseline(baselines.get().apiLintFile, targetsJavaConsumers.get()), + generateRestrictToLibraryGroupAPIs, + levelsArgs, + k2UastEnabled.get(), + kotlinSourceLevel.get(), + workerExecutor, + manifestPath.orNull?.asFile?.absolutePath, + multiplatform.get(), + ) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt new file mode 100644 index 0000000000000..db5970058d534 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt @@ -0,0 +1,483 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.Version +import androidx.build.checkapi.ApiLocation +import androidx.build.getLibraryClasspath +import androidx.build.logging.TERMINAL_RED +import androidx.build.logging.TERMINAL_RESET +import java.io.ByteArrayOutputStream +import java.io.File +import javax.inject.Inject +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.process.ExecOperations +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + +// MetalavaRunner stores common configuration for executing Metalava + +fun runMetalavaWithArgs( + metalavaClasspath: FileCollection, + args: List, + k2UastEnabled: Boolean, + kotlinSourceLevel: KotlinVersion, + workerExecutor: WorkerExecutor, +) { + val allArgs = + args + + listOf( + "--hide", + // Removing final from a method does not cause compatibility issues for AndroidX. + "RemovedFinalStrict", + "--error", + "UnresolvedImport", + "--kotlin-source", + kotlinSourceLevel.version, + + // Metalava arguments to suppress compatibility checks for experimental API + // surfaces. + "--suppress-compatibility-meta-annotation", + "androidx.annotation.RequiresOptIn", + "--suppress-compatibility-meta-annotation", + "kotlin.RequiresOptIn", + + // Skip reading comments in Metalava for two reasons: + // - We prefer for developers to specify api information via annotations instead + // of just javadoc comments (like @hide) + // - This allows us to improve cacheability of Metalava tasks + "--ignore-comments", + "--hide", + "DeprecationMismatch", + "--hide", + "DocumentExceptions", + + // Don't track annotations that aren't needed for review or checking compat. + "--exclude-annotation", + "androidx.annotation.ReplaceWith", + "--exclude-annotation", + "androidx.compose.runtime.ComposableInferredTarget", + // internal annotation, includes debug information and values are not constant + "--exclude-annotation", + "androidx.compose.runtime.internal.FunctionKeyMeta", + + // This issue is important for stubs generation, which we don't do here. + "--hide", + "InheritChangesSignature", + ) + val workQueue = workerExecutor.processIsolation() + workQueue.submit(MetalavaWorkAction::class.java) { parameters -> + parameters.args.set(allArgs) + parameters.metalavaClasspath.set(metalavaClasspath.files) + parameters.k2UastEnabled.set(k2UastEnabled) + } +} + +interface MetalavaParams : WorkParameters { + val args: ListProperty + val metalavaClasspath: SetProperty + val k2UastEnabled: Property +} + +abstract class MetalavaWorkAction @Inject constructor(private val execOperations: ExecOperations) : + WorkAction { + override fun execute() { + val outputStream = ByteArrayOutputStream() + var successful = false + // Enable Android Lint infrastructure used by Metalava to use K2 or K1 UAST (K1 support will + // be deprecated once all projects are switched to K2 b/385140979). + val k2UastArg = + if (parameters.k2UastEnabled.get()) { + "--Xuse-k2-uast" + } else { + "--Xuse-k1-uast" + } + try { + execOperations.javaexec { + // Intellij core reflects into java.util.ResourceBundle + it.jvmArgs = listOf("--add-opens", "java.base/java.util=ALL-UNNAMED") + it.systemProperty("java.awt.headless", "true") + it.classpath(parameters.metalavaClasspath.get()) + it.mainClass.set("com.android.tools.metalava.Driver") + it.args = parameters.args.get() + k2UastArg + it.setStandardOutput(outputStream) + it.setErrorOutput(outputStream) + } + successful = true + } finally { + if (!successful) { + System.err.println(outputStream.toString(Charsets.UTF_8)) + } + } + } +} + +fun Project.getMetalavaClasspath(): FileCollection = getLibraryClasspath("metalava") + +fun getApiLintArgs(targetsJavaConsumers: Boolean): List { + val args = + mutableListOf( + "--api-lint", + "--hide", + listOf( + // The list of checks that are hidden as they are not useful in androidx + "Enum", // Enums are allowed to be use in androidx + "CallbackInterface", // With target Java 8, we have default methods + "ProtectedMember", // We allow using protected members in androidx + "ManagerLookup", // Managers in androidx are not the same as platform services + "ManagerConstructor", + "RethrowRemoteException", // This check is for calls into system_server + "PackageLayering", // This check is not relevant to androidx.* code. + "UserHandle", // This check is not relevant to androidx.* code. + "ParcelableList", // This check is only relevant to android platform that has + // managers. + + // List of checks that have bugs, but should be enabled once fixed. + "StaticUtils", // b/135489083 + "StartWithLower", // b/135710527 + + // The list of checks that are API lint warnings and are yet to be enabled + "SamShouldBeLast", + + // We should only treat these as warnings + "IntentBuilderName", + "OnNameExpected", + "UserHandleName", + ) + .joinToString(), + "--error", + listOf( + "AllUpper", + "GetterSetterNames", + "MinMaxConstant", + "TopLevelBuilder", + "BuilderSetStyle", + "MissingBuildMethod", + "SetterReturnsThis", + "OverlappingConstants", + "ListenerLast", + "ExecutorRegistration", + "StreamFiles", + "AbstractInner", + "NotCloseable", + "MethodNameTense", + "UseIcu", + "NoByteOrShort", + "GetterOnBuilder", + "CallbackMethodName", + "StaticFinalBuilder", + "MissingGetterMatchingBuilder", + "HiddenSuperclass", + "KotlinOperator", + "DataClassDefinition", + "TypeParameterName", + ) + .joinToString(), + ) + // Acronyms that can be used in their all-caps form. "SQ" is included to allow "SQLite". + val allowedAcronyms = listOf("SQL", "SQ", "URL", "EGL", "GL", "KHR") + for (acronym in allowedAcronyms) { + args.add("--api-lint-allowed-acronym") + args.add(acronym) + } + val javaOnlyIssues = + listOf( + "MissingJvmstatic", + "ArrayReturn", + "ValueClassDefinition", + "FacadeClassJvmName", + "ValueClassUsageFromConstructor", + "ValueClassUsageWithoutJvmName", + ) + val javaOnlyErrorLevel = + if (targetsJavaConsumers) { + "--error" + } else { + "--hide" + } + args.add(javaOnlyErrorLevel) + args.add(javaOnlyIssues.joinToString()) + return args +} + +/** Returns the args needed to generate a version history JSON from the previous API files. */ +internal fun getGenerateApiLevelsArgs( + apiDir: File, + apiFiles: List, + currentVersion: Version, + outputLocation: File, +): List { + return buildList { + add("--generate-api-version-history") + add(outputLocation.absolutePath) + add("--api-version-for-sources") + add(currentVersion.toString()) + if (apiFiles.isNotEmpty()) { + add("--api-version-signature-files") + add(apiFiles.joinToString(":")) + add("--api-version-signature-pattern") + // Select the version from the files. The `*` wildcard matches and ignores any + // pre-release suffix. + add("$apiDir/{version:major.minor.patch}*.txt") + } + } +} + +sealed class GenerateApiMode { + object PublicApi : GenerateApiMode() + + object AllRestrictedApis : GenerateApiMode() + + object RestrictToLibraryGroupPrefixApis : GenerateApiMode() +} + +sealed class ApiLintMode { + class CheckBaseline(val apiLintBaseline: File, val targetsJavaConsumers: Boolean) : + ApiLintMode() + + object Skip : ApiLintMode() +} + +/** + * Generates all of the specified api files, as well as a version history JSON for the public API. + */ +internal fun generateApi( + metalavaClasspath: FileCollection, + projectXml: File, + sourcePaths: Collection, + compiledSources: File?, + apiLocation: ApiLocation, + apiLintMode: ApiLintMode, + includeRestrictToLibraryGroupApis: Boolean, + apiLevelsArgs: List, + k2UastEnabled: Boolean, + kotlinSourceLevel: KotlinVersion, + workerExecutor: WorkerExecutor, + pathToManifest: String? = null, + multiplatform: Boolean, +) { + val generateApiConfigs: MutableList> = + mutableListOf(GenerateApiMode.PublicApi to apiLintMode) + + @Suppress("LiftReturnOrAssignment") + if (includeRestrictToLibraryGroupApis) { + generateApiConfigs += GenerateApiMode.AllRestrictedApis to ApiLintMode.Skip + } else { + generateApiConfigs += GenerateApiMode.RestrictToLibraryGroupPrefixApis to ApiLintMode.Skip + } + + generateApiConfigs.forEach { (generateApiMode, apiLintMode) -> + generateApi( + metalavaClasspath, + projectXml, + sourcePaths, + compiledSources, + apiLocation, + generateApiMode, + apiLintMode, + apiLevelsArgs, + k2UastEnabled, + kotlinSourceLevel, + workerExecutor, + pathToManifest, + multiplatform, + ) + } +} + +/** + * Gets arguments for generating the specified api file (and a version history JSON if the + * [generateApiMode] is [GenerateApiMode.PublicApi]. + */ +private fun generateApi( + metalavaClasspath: FileCollection, + projectXml: File, + sourcePaths: Collection, + compiledSources: File?, + outputLocation: ApiLocation, + generateApiMode: GenerateApiMode, + apiLintMode: ApiLintMode, + apiLevelsArgs: List, + k2UastEnabled: Boolean, + kotlinSourceLevel: KotlinVersion, + workerExecutor: WorkerExecutor, + pathToManifest: String? = null, + multiplatform: Boolean, +) { + val args = + getGenerateApiArgs( + projectXml, + sourcePaths, + compiledSources, + outputLocation, + generateApiMode, + apiLintMode, + apiLevelsArgs, + pathToManifest, + multiplatform, + ) + runMetalavaWithArgs(metalavaClasspath, args, k2UastEnabled, kotlinSourceLevel, workerExecutor) +} + +/** + * Generates the specified api file, and a version history JSON if the [generateApiMode] is + * [GenerateApiMode.PublicApi]. + */ +fun getGenerateApiArgs( + projectXml: File, + sourcePaths: Collection, + compiledSources: File?, + outputLocation: ApiLocation?, + generateApiMode: GenerateApiMode, + apiLintMode: ApiLintMode, + apiLevelsArgs: List, + pathToManifest: String? = null, + multiplatform: Boolean, +): List { + // generate public API txt + val args = + mutableListOf( + "--source-path", + sourcePaths.filter { it.exists() }.joinToString(File.pathSeparator), + "--project", + projectXml.path, + ) + + // Include the jar file to generate bytecode-only APIs if this project has any Kotlin source. + if (compiledSources != null && sourcePaths.any { containsKotlinFiles(it) }) { + args += listOf("--compiled-sources", compiledSources.absolutePath) + } + + args += listOf("--format=v4", "--warnings-as-errors") + + pathToManifest?.let { args += listOf("--manifest", pathToManifest) } + + if (outputLocation != null) { + when (generateApiMode) { + is GenerateApiMode.PublicApi -> { + args += listOf("--api", outputLocation.publicApiFile.toString()) + // Generate API levels just for the public API + args += apiLevelsArgs + } + is GenerateApiMode.AllRestrictedApis, + GenerateApiMode.RestrictToLibraryGroupPrefixApis -> { + args += listOf("--api", outputLocation.restrictedApiFile.toString()) + } + } + } + + when (generateApiMode) { + is GenerateApiMode.PublicApi -> { + args += listOf("--hide-annotation", "androidx.annotation.RestrictTo") + args += listOf("--show-unannotated") + + // Run multiplatform lint for the public API invocation of metalava. + if (multiplatform) { + args += "--multiplatform-enabled" + } + } + is GenerateApiMode.AllRestrictedApis, + GenerateApiMode.RestrictToLibraryGroupPrefixApis -> { + // Despite being hidden we still track the following: + // * @RestrictTo(Scope.LIBRARY_GROUP_PREFIX): inter-library APIs + // * @PublishedApi: needs binary stability for inline methods + // * @RestrictTo(Scope.LIBRARY_GROUP): APIs between libraries in non-atomic groups + args += + listOf( + // hide RestrictTo(LIBRARY), use --show-annotation for RestrictTo with + // specific arguments + "--hide-annotation", + "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)", + "--show-annotation", + "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." + + "LIBRARY_GROUP_PREFIX)", + "--show-annotation", + "kotlin.PublishedApi", + "--show-unannotated", + ) + if (generateApiMode is GenerateApiMode.AllRestrictedApis) { + args += + listOf( + "--show-annotation", + "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." + + "LIBRARY_GROUP)", + ) + } else { + args += + listOf( + "--hide-annotation", + "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." + + "LIBRARY_GROUP)", + ) + } + } + } + + when (apiLintMode) { + is ApiLintMode.CheckBaseline -> { + args += getApiLintArgs(apiLintMode.targetsJavaConsumers) + if (apiLintMode.apiLintBaseline.exists()) { + args += listOf("--baseline", apiLintMode.apiLintBaseline.toString()) + } + args.addAll( + listOf( + "--error", + "ReferencesDeprecated", + "--error-message:api-lint", + """ + ${TERMINAL_RED}Your change has API lint issues. Fix the code according to the messages above.$TERMINAL_RESET + + If a check is broken, suppress it in code in Kotlin with @Suppress("id")/@get:Suppress("id") + and in Java with @SuppressWarnings("id") and file bug to + https://issuetracker.google.com/issues/new?component=739152&template=1344623 + + If you are doing a refactoring or suppression above does not work, use ./gradlew updateApiLintBaseline +""", + ) + ) + } + is ApiLintMode.Skip -> { + args.addAll( + listOf( + "--hide", + "UnhiddenSystemApi", + "--hide", + "ReferencesHidden", + "--hide", + "ReferencesDeprecated", + ) + ) + } + } + + return args +} + +/** Whether the [file] is a kotlin file or is a directory containing one (recursively). */ +private fun containsKotlinFiles(file: File): Boolean { + return if (file.isDirectory) { + file.listFiles().any { containsKotlinFiles(it) } + } else { + file.extension == "kt" + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTask.kt new file mode 100644 index 0000000000000..9efc05e4114db --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTask.kt @@ -0,0 +1,225 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.Version +import androidx.build.checkapi.ApiBaselinesLocation +import androidx.build.checkapi.ApiLocation +import androidx.build.checkapi.SourceSetInputs +import java.io.File +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.FileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + +/** Base class for invoking Metalava. */ +@CacheableTask +abstract class MetalavaTask +@Inject +constructor(@Internal protected val workerExecutor: WorkerExecutor) : DefaultTask() { + /** Classpath containing Metalava and its dependencies. */ + @get:Classpath abstract val metalavaClasspath: ConfigurableFileCollection + + /** Android's boot classpath */ + @get:Classpath lateinit var bootClasspath: FileCollection + + /** Dependencies (compiled classes) of the project. */ + @get:Classpath lateinit var dependencyClasspath: FileCollection + + @get:Input abstract val k2UastEnabled: Property + + @get:Input abstract val kotlinSourceLevel: Property + + @get:Input abstract val targetsJavaConsumers: Property + + fun runWithArgs(args: List) { + runMetalavaWithArgs( + metalavaClasspath, + args, + k2UastEnabled.get(), + kotlinSourceLevel.get(), + workerExecutor, + ) + } +} + +/** A metalava task that takes source code as input (other tasks take signature files). */ +@CacheableTask +internal abstract class SourceMetalavaTask(workerExecutor: WorkerExecutor) : + MetalavaTask(workerExecutor) { + /** + * Specifies both the source files and their corresponding compiled class files + * + * We specify the source files to pass to Metalava because that's the format that Metalava + * needs. + * + * However, Metalava is only supposed to read the public API, so we don't need to rerun Metalava + * if no API changes occurred. + * + * Gradle doesn't offer all of the same abilities as Metalava for writing a signature file and + * validating its compatibility, but Gradle does offer the ability to check whether two sets of + * classes have the same API. + * + * So, we ask Gradle to rerun this task only if the public API changes, which we implement by + * declaring the compiled classes as inputs rather than the sources + */ + /** Source files against which API signatures will be validated. */ + @get:Internal // UP-TO-DATE checking is done based on the compiled classes + var sourcePaths: FileCollection = project.files() + + /** Class files compiled from sourcePaths */ + @get:Classpath var compiledSources: FileCollection = project.files() + + @get:[Optional InputFile PathSensitive(PathSensitivity.NONE)] + abstract val manifestPath: RegularFileProperty + + @get:Internal // already expressed by getApiLintBaseline() + abstract val baselines: Property + + @Optional + @PathSensitive(PathSensitivity.NONE) + @InputFile + fun getInputApiLintBaseline(): File? { + val baseline = baselines.get().apiLintFile + return if (baseline.exists()) baseline else null + } + + /** + * Information about all source sets for multiplatform projects. Non-multiplatform projects + * should be represented as a list with one source set. + * + * This is marked as [Internal] because [compiledSources] is what should determine whether to + * rerun metalava. + */ + @get:Internal abstract val sourceSets: ListProperty + + /** Whether metalava should process the project as multiplatform. */ + @get:Input abstract val multiplatform: Property + + /** + * Creates an XML file representing the project structure. + * + * This should only be called during task execution. + */ + protected fun createProjectXmlFile(): File { + val sourceSets = sourceSets.get() + check(sourceSets.isNotEmpty()) { "Project must have at least one source set." } + val outputFile = File(temporaryDir, "project.xml") + ProjectXml.create(sourceSets, bootClasspath.files, compiledSources.singleFile, outputFile) + return outputFile + } +} + +/** A metalava task that uses signature files to run compatibility checks. */ +@CacheableTask +internal abstract class CompatibilityMetalavaTask(workerExecutor: WorkerExecutor) : + MetalavaTask(workerExecutor) { + /** Location of the previous API surface for compatibility checks. */ + @get:Internal // already expressed by getTaskInputs() + abstract val referenceApi: Property + + /** Location of the current API surface to check. */ + @get:Internal // already expressed by getTaskInputs() + abstract val api: Property + + /** Location of the text files listing violations that should be ignored. */ + @get:Internal // already expressed by getTaskInputs() + abstract val baselines: Property + + /** Version for the current API surface. */ + @get:Input abstract val version: Property + + @PathSensitive(PathSensitivity.RELATIVE) + @InputFiles + fun getTaskInputs(): List { + val apiLocation = api.get() + val referenceApiLocation = referenceApi.get() + val baselineApiLocation = baselines.get() + return listOf( + apiLocation.publicApiFile, + apiLocation.restrictedApiFile, + referenceApiLocation.publicApiFile, + referenceApiLocation.restrictedApiFile, + baselineApiLocation.publicApiFile, + baselineApiLocation.restrictedApiFile, + ) + } + + /** Whether there are restricted APIs to check. */ + protected fun restrictedApisExist(): Boolean = referenceApi.get().restrictedApiFile.exists() + + /** Returns the baseline file to use, depending on whether it is for [restricted] APIs. */ + protected fun getBaselineFile(restricted: Boolean): File { + return if (restricted) { + baselines.get().restrictedApiFile + } else { + baselines.get().publicApiFile + } + } + + /** + * Returns the list of common arguments for compatibility tasks. + * + * @param restricted whether this compatibility check is for restricted APIs + * @param freezeApis whether APIs are frozen and no changes should be allowed + */ + protected fun getCompatibilityArguments( + restricted: Boolean, + freezeApis: Boolean, + ): List { + val (currentSignature, previousSignature) = + if (restricted) { + api.get().restrictedApiFile to referenceApi.get().restrictedApiFile + } else { + api.get().publicApiFile to referenceApi.get().publicApiFile + } + + return buildList { + add("--classpath") + add((bootClasspath + dependencyClasspath.files).joinToString(File.pathSeparator)) + add("--source-files") + add(currentSignature.toString()) + add("--check-compatibility:api:released") + add(previousSignature.toString()) + add("--warnings-as-errors") + + if (freezeApis) { + add("--error-category") + add("Compatibility") + } + + if (!targetsJavaConsumers.get()) { + add("--hide") + add("RemovedFromJava") + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt new file mode 100644 index 0000000000000..5974990d54139 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt @@ -0,0 +1,250 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.AndroidXExtension +import androidx.build.addFilterableTasks +import androidx.build.addToBuildOnServer +import androidx.build.addToCheckTask +import androidx.build.checkapi.ApiBaselinesLocation +import androidx.build.checkapi.ApiLocation +import androidx.build.checkapi.CompilationInputs +import androidx.build.checkapi.MultiplatformCompilationInputs +import androidx.build.checkapi.SourceSetInputs +import androidx.build.checkapi.getRequiredCompatibilityApiLocation +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import androidx.build.version +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.TaskProvider +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType + +internal object MetalavaTasks { + + fun setupProject( + project: Project, + compilationInputs: CompilationInputs, + generateApiDependencies: Configuration, + extension: AndroidXExtension, + androidManifest: Provider?, + baselinesApiLocation: ApiBaselinesLocation, + builtApiLocation: ApiLocation, + outputApiLocations: List, + ) { + val metalavaClasspath = project.getMetalavaClasspath() + val version = project.version() + + // Policy: If the artifact belongs to an atomic (e.g. same-version) group, we don't enforce + // binary compatibility for APIs annotated with @RestrictTo(LIBRARY_GROUP). This is + // implemented by excluding APIs with this annotation from the restricted API file. + val generateRestrictToLibraryGroupAPIs = !extension.mavenGroup!!.requireSameVersion + val kotlinSourceLevel: Provider = extension.kotlinApiVersion + val targetsJavaConsumers = extension.type.map { !it.targetsKotlinConsumersOnly } + // For a KMP project, only use multiplatform metalava if K2 is also used as K1 metalava does + // not support multiplatform. + val multiplatform = + extension.metalavaK2UastEnabled.map { + it && compilationInputs is MultiplatformCompilationInputs + } + val generateApi = + project.tasks.register("generateApi", GenerateApiTask::class.java) { task -> + task.group = "API" + task.description = "Generates API files from source" + task.apiLocation.set(builtApiLocation) + task.metalavaClasspath.from(metalavaClasspath) + task.generateRestrictToLibraryGroupAPIs = generateRestrictToLibraryGroupAPIs + task.baselines.set(baselinesApiLocation) + task.targetsJavaConsumers.set(targetsJavaConsumers) + task.k2UastEnabled.set(extension.metalavaK2UastEnabled) + task.kotlinSourceLevel.set(kotlinSourceLevel) + task.multiplatform.set(multiplatform) + + // Arguments needed for generating the API levels JSON + task.projectApiDirectory = project.layout.projectDirectory.dir("api") + task.currentVersion.set(version) + + applyInputs(compilationInputs, task, generateApiDependencies, androidManifest) + // If we will be updating the api lint baselines, then we should do that before + // using it to validate the generated api + task.mustRunAfter("updateApiLintBaseline") + } + project.registerVersionMetadataComponent(generateApi) + + // Policy: If the artifact has previously been released, e.g. has a beta or later API file + // checked in, then we must verify "release compatibility" against the work-in-progress + // API file. + var checkApiRelease: TaskProvider? = null + var ignoreApiChanges: TaskProvider? = null + project.getRequiredCompatibilityApiLocation()?.let { lastReleasedApiFile -> + checkApiRelease = + project.tasks.register("checkApiRelease", CheckApiCompatibilityTask::class.java) { + task -> + task.metalavaClasspath.from(metalavaClasspath) + task.referenceApi.set(lastReleasedApiFile) + task.baselines.set(baselinesApiLocation) + task.api.set(builtApiLocation) + task.version.set(version) + task.dependencyClasspath = compilationInputs.dependencyClasspath + task.bootClasspath = compilationInputs.bootClasspath + task.k2UastEnabled.set(extension.metalavaK2UastEnabled) + task.kotlinSourceLevel.set(kotlinSourceLevel) + task.targetsJavaConsumers.set(targetsJavaConsumers) + task.cacheEvenIfNoOutputs() + task.dependsOn(generateApi) + } + + ignoreApiChanges = + project.tasks.register("ignoreApiChanges", IgnoreApiChangesTask::class.java) { task + -> + task.metalavaClasspath.from(metalavaClasspath) + task.referenceApi.set(checkApiRelease!!.flatMap { it.referenceApi }) + task.baselines.set(checkApiRelease!!.flatMap { it.baselines }) + task.api.set(builtApiLocation) + task.version.set(version) + task.dependencyClasspath = compilationInputs.dependencyClasspath + task.bootClasspath = compilationInputs.bootClasspath + task.k2UastEnabled.set(extension.metalavaK2UastEnabled) + task.kotlinSourceLevel.set(kotlinSourceLevel) + task.targetsJavaConsumers.set(targetsJavaConsumers) + task.dependsOn(generateApi) + } + } + + val updateApiLintBaseline = + project.tasks.register( + "updateApiLintBaseline", + UpdateApiLintBaselineTask::class.java, + ) { task -> + task.metalavaClasspath.from(metalavaClasspath) + task.baselines.set(baselinesApiLocation) + task.targetsJavaConsumers.set(targetsJavaConsumers) + task.k2UastEnabled.set(extension.metalavaK2UastEnabled) + task.kotlinSourceLevel.set(kotlinSourceLevel) + task.multiplatform.set(multiplatform) + applyInputs(compilationInputs, task, generateApiDependencies, androidManifest) + } + + // Policy: All changes to API surfaces for which compatibility is enforced must be + // explicitly confirmed by running the updateApi task. To enforce this, the implementation + // checks the "work-in-progress" built API file against the checked in current API file. + val checkApi = + project.tasks.register("checkApi", CheckApiEquivalenceTask::class.java) { task -> + task.group = "API" + task.description = + "Checks that the API generated from source code matches the " + + "checked in API file" + task.builtApi.set(generateApi.flatMap { it.apiLocation }) + task.cacheEvenIfNoOutputs() + task.checkedInApis.set(outputApiLocations) + task.dependsOn(generateApi) + checkApiRelease?.let { task.dependsOn(checkApiRelease) } + } + + val regenerateOldApis = + project.tasks.register("regenerateOldApis", RegenerateOldApisTask::class.java) { task -> + task.group = "API" + task.description = + "Regenerates historic API .txt files using the " + + "corresponding prebuilt and the latest Metalava" + task.kotlinSourceLevel.set(kotlinSourceLevel) + task.generateRestrictToLibraryGroupAPIs = generateRestrictToLibraryGroupAPIs + } + + // ignoreApiChanges depends on the output of this task for the "last released" API + // surface. Make sure it always runs *after* the regenerateOldApis task. + ignoreApiChanges?.configure { it.mustRunAfter(regenerateOldApis) } + + // checkApiRelease validates the output of this task, so make sure it always runs + // *after* the regenerateOldApis task. + checkApiRelease?.configure { it.mustRunAfter(regenerateOldApis) } + + val updateApi = + project.tasks.register("updateApi", UpdateApiTask::class.java) { task -> + task.group = "API" + task.description = "Updates the checked in API files to match source code API" + task.inputApiLocation.set(generateApi.flatMap { it.apiLocation }) + task.outputApiLocations.set(checkApi.flatMap { it.checkedInApis }) + task.dependsOn(generateApi) + + // If a developer (accidentally) makes a non-backwards compatible change to an API, + // the developer will want to be informed of it as soon as possible. So, whenever a + // developer updates an API, if backwards compatibility checks are enabled in the + // library, then we want to check that the changes are backwards compatible. + checkApiRelease?.let { task.dependsOn(it) } + } + + // ignoreApiChanges depends on the output of this task for the "current" API surface. + // Make sure it always runs *after* the updateApi task. + ignoreApiChanges?.configure { it.mustRunAfter(updateApi) } + + val regenerateApis = + project.tasks.register("regenerateApis") { task -> + task.group = "API" + task.description = + "Regenerates current and historic API .txt files using the corresponding " + + "prebuilt and the latest Metalava, then updates API ignore files" + task.dependsOn(regenerateOldApis) + task.dependsOn(updateApi) + ignoreApiChanges?.let { task.dependsOn(it) } + } + + project.addToCheckTask(checkApi) + project.addToBuildOnServer(checkApi) + project.addFilterableTasks( + ignoreApiChanges, + updateApiLintBaseline, + checkApi, + regenerateOldApis, + updateApi, + regenerateApis, + generateApi, + ) + } + + private fun applyInputs( + inputs: CompilationInputs, + task: SourceMetalavaTask, + generateApiDependencies: Configuration, + androidManifest: Provider?, + ) { + task.sourcePaths = inputs.sourcePaths + task.compiledSources = generateApiDependencies + task.bootClasspath = inputs.bootClasspath + androidManifest?.let { task.manifestPath.set(it) } + if (inputs is MultiplatformCompilationInputs) { + task.dependencyClasspath = inputs.allSourceSetsDependencyClasspath + task.sourceSets.set(inputs.sourceSets) + } else { + task.dependencyClasspath = inputs.dependencyClasspath + // Represent a non-multiplatform project as one source set. + task.sourceSets.set( + listOf( + SourceSetInputs( + sourceSetName = "main", + dependsOnSourceSets = emptyList(), + sourcePaths = inputs.sourcePaths, + dependencyClasspath = inputs.dependencyClasspath, + kotlinPlatforms = setOf(KotlinPlatformType.androidJvm), + ) + ) + ) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/ProjectXml.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/ProjectXml.kt new file mode 100644 index 0000000000000..a7a151553fe67 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/ProjectXml.kt @@ -0,0 +1,230 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.checkapi.SourceSetInputs +import com.google.common.annotations.VisibleForTesting +import java.io.File +import java.io.Writer +import org.dom4j.DocumentHelper +import org.dom4j.Element +import org.dom4j.io.OutputFormat +import org.dom4j.io.XMLWriter +import org.gradle.api.file.FileCollection +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType + +internal object ProjectXml { + /** + * Generates an XML file representing the structure of a KMP project, to be used by metalava. + * + * For more information see go/metalavatask-kmp-spec. + */ + fun create( + sourceSets: List, + bootClasspath: Collection, + compiledSourceJar: File, + outputFile: File, + ) { + // Compute the files for each source set initially so they can be checked multiple times + // without recomputing. + val sourceSetFiles = + sourceSets.associate { sourceSet -> + sourceSet.sourceSetName to sourceFiles(sourceSet.sourcePaths) + } + val filteredSourceSets = filterSourceSets(sourceSets, sourceSetFiles) + val sourceSetElements = + filteredSourceSets.map { sourceSet -> + val sourceSetDependencies = sourceSet.dependencyClasspath.files + // Include Android jars only for JVM and Android source sets (they are needed for + // JVM because they provide the java standard libraries). + val allDependencies = + if ( + KotlinPlatformType.jvm in sourceSet.kotlinPlatforms || + KotlinPlatformType.androidJvm in sourceSet.kotlinPlatforms + ) { + sourceSetDependencies + bootClasspath + } else { + sourceSetDependencies + } + createSourceSetElement( + sourceSet.sourceSetName, + sourceSet.dependsOnSourceSets, + sourceSetFiles[sourceSet.sourceSetName]!!, + allDependencies, + compiledSourceJar, + sourceSet.kotlinPlatforms, + ) + } + val projectElement = createProjectElement(sourceSetElements) + writeXml(projectElement, outputFile.writer()) + } + + /** + * Returns a filtered list of source sets, removing those that have no source files and are not + * depended on by any other source sets. + */ + @VisibleForTesting + fun filterSourceSets( + sourceSets: List, + sourceSetFiles: Map>, + ): List { + val filtered = + sourceSets.filter { sourceSet -> + // Include any source sets with source files. + sourceSetFiles[sourceSet.sourceSetName]!!.isNotEmpty() || + // Include any source sets that are depended on by another source set. + sourceSets.any { otherSourceSet -> + sourceSet.sourceSetName in otherSourceSet.dependsOnSourceSets + } || + // Include androidMain, even if it has no source files, to prevent errors that + // come from excluding the primary source set for the android compilation. + sourceSet.sourceSetName == "androidMain" + } + // If any source sets were filtered, do another pass as there may be source sets which were + // previously depended on by filtered source sets which now can also be filtered. + return if (filtered.size == sourceSets.size) { + filtered + } else { + filterSourceSets(filtered, sourceSetFiles) + } + } + + /** Writes the [element] as XML to the [writer] and closes the stream. */ + @VisibleForTesting + fun writeXml(element: Element, writer: Writer) { + val document = DocumentHelper.createDocument(element) + XMLWriter(writer, OutputFormat(/* indent= */ " ", /* newlines= */ true)).apply { + write(document) + close() + } + } + + /** Constructs the XML [Element] for the project. */ + @VisibleForTesting + fun createProjectElement(sourceSets: List): Element { + val projectElement = DocumentHelper.createElement("project") + + // Setting "." for the root dir is equivalent to using the project directory path. + val rootDirElement = DocumentHelper.createElement("root") + rootDirElement.addAttribute("dir", ".") + projectElement.add(rootDirElement) + + for (sourceSet in sourceSets) { + projectElement.add(sourceSet) + } + + return projectElement + } + + /** Constructs the XML [Element] representing one source set. */ + @VisibleForTesting + fun createSourceSetElement( + sourceSetName: String, + dependsOnSourceSets: Collection, + sourceFiles: Collection, + allDependencies: Collection, + compiledSourceJar: File, + kotlinPlatforms: Set, + ): Element { + val moduleElement = DocumentHelper.createElement("module") + moduleElement.addAttribute("name", sourceSetName) + if (sourceSetName == "androidMain") { + moduleElement.addAttribute("android", "true") + } + // Create the /-separated string listing all Kotlin platform types that this source set can + // be part of. The serializations are from the commented-out Kotlin compiler classes. The + // compiler is a compile only dependency for this project, so to generate the strings + // instead of hardcoding them it would need to be a runtime dependency as well. + val kotlinPlatformStrings = + kotlinPlatforms + .mapNotNull { + when (it) { + // JvmPlatforms.defaultJvmPlatform + KotlinPlatformType.jvm, + KotlinPlatformType.androidJvm -> "JVM [1.8]" + // NativePlatforms.unspecifiedNativePlatform + KotlinPlatformType.native -> "Native []/Native [general]" + // JsPlatforms.defaultJsPlatform + KotlinPlatformType.js -> "JS []" + // WasmPlatforms.unspecifiedWasmPlatform + KotlinPlatformType.wasm -> "Wasm [general]" + else -> null + } + } + .toSet() + moduleElement.addAttribute("kotlinPlatforms", kotlinPlatformStrings.joinToString("/")) + + for (dependsOn in dependsOnSourceSets) { + val depElement = DocumentHelper.createElement("dep") + depElement.addAttribute("module", dependsOn) + depElement.addAttribute("kind", "dependsOn") + moduleElement.add(depElement) + } + + for (sourceFile in sourceFiles) { + val srcElement = DocumentHelper.createElement("src") + srcElement.addAttribute("file", sourceFile.absolutePath) + moduleElement.add(srcElement) + } + + for (dependency in allDependencies) { + val (elementType, fileType) = + when (dependency.extension) { + "jar" -> "classpath" to "jar" + "klib" -> "klib" to "file" + "aar" -> "classpath" to "aar" + "" -> "classpath" to "dir" + else -> continue + } + + val dependencyElement = DocumentHelper.createElement(elementType) + dependencyElement.addAttribute(fileType, dependency.absolutePath) + moduleElement.add(dependencyElement) + } + + // Adding the compiled sources of this project fixes issues where annotations on some + // elements aren't registered by metalava (e.g. in :ink:ink-rendering). + val jarElement = DocumentHelper.createElement("src") + jarElement.addAttribute("jar", compiledSourceJar.absolutePath) + moduleElement.add(jarElement) + + return moduleElement + } + + /** Lists all of the files from [sources]. */ + private fun sourceFiles(sources: FileCollection): List { + return sources.files.flatMap { gatherFiles(it) } + } + + /** + * If [file] is a normal file, returns a list containing [file]. + * + * If [file] is a directory, returns a list of all normal files recursively contained in the + * directory. + * + * Otherwise, returns an empty list. + */ + private fun gatherFiles(file: File): List { + return if (file.isFile) { + listOf(file) + } else if (file.isDirectory) { + file.listFiles()?.flatMap { gatherFiles(it) } ?: emptyList() + } else { + emptyList() + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt new file mode 100644 index 0000000000000..44e1a99dd9045 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt @@ -0,0 +1,306 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.Version +import androidx.build.checkapi.ApiLocation +import androidx.build.checkapi.SourceSetInputs +import androidx.build.checkapi.getApiFileVersion +import androidx.build.checkapi.getRequiredCompatibilityApiLocation +import androidx.build.checkapi.getVersionedApiLocation +import androidx.build.checkapi.isValidArtifactVersion +import androidx.build.getAndroidJar +import androidx.build.getCheckoutRoot +import java.io.File +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.artifacts.ivyservice.TypedResolveException +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.api.tasks.util.PatternFilterable +import org.gradle.workers.WorkerExecutor +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType + +/** Generate API signature text files using previously built .jar/.aar artifacts. */ +@CacheableTask +abstract class RegenerateOldApisTask +@Inject +constructor(private val workerExecutor: WorkerExecutor) : DefaultTask() { + + @Input var generateRestrictToLibraryGroupAPIs = true + + @get:Input abstract val kotlinSourceLevel: Property + + @get:Input + @set:Option( + option = "compat-version", + description = "Regenerate just the signature file needed for compatibility checks", + ) + var compatVersion: Boolean = false + + @TaskAction + fun exec() { + val groupId = project.group.toString() + val artifactId = project.name + val internalPrebuiltsDir = File(project.getCheckoutRoot(), "prebuilts/androidx/internal") + val projectPrebuiltsDir = + File(internalPrebuiltsDir, groupId.replace(".", "/") + "/" + artifactId) + if (compatVersion) { + regenerateCompatVersion(groupId, artifactId, projectPrebuiltsDir) + } else { + regenerateAllVersions(groupId, artifactId, projectPrebuiltsDir) + } + } + + /** + * Attempts to regenerate the API file for all previous versions by listing the prebuilt + * versions that exist and regenerating each one which already has an existing signature file. + */ + private fun regenerateAllVersions( + groupId: String, + artifactId: String, + projectPrebuiltsDir: File, + ) { + val artifactVersions = listVersions(projectPrebuiltsDir) + + var prevApiFileVersion = getApiFileVersion(project.version as Version) + for (artifactVersion in artifactVersions.reversed()) { + val apiFileVersion = getApiFileVersion(artifactVersion) + // If two artifacts correspond to the same API file, don't regenerate the + // same api file again + if (apiFileVersion != prevApiFileVersion) { + val location = project.getVersionedApiLocation(apiFileVersion) + regenerate(project.rootProject, groupId, artifactId, artifactVersion, location) + prevApiFileVersion = apiFileVersion + } + } + } + + /** + * Regenerates just the signature file used for compatibility checks against the current + * version. If prebuilts for that version don't exist (since prebuilts for betas are sometimes + * deleted), attempts to use prebuilts for the corresponding stable version, which should have + * the same API surface. + */ + private fun regenerateCompatVersion( + groupId: String, + artifactId: String, + projectPrebuiltsDir: File, + ) { + val location = + project.getRequiredCompatibilityApiLocation() + ?: run { + logger.warn("No required compat location for $groupId:$artifactId") + return + } + val compatVersion = location.version()!! + + if (!tryRegenerate(projectPrebuiltsDir, groupId, artifactId, compatVersion, location)) { + val stable = compatVersion.copy(preRelease = null) + logger.warn("No prebuilts for version $compatVersion, trying with $stable") + if (!tryRegenerate(projectPrebuiltsDir, groupId, artifactId, stable, location)) { + logger.error("Could not regenerate $compatVersion") + } + } + } + + /** + * If prebuilts exists for the [version], runs [regenerate] and returns true, otherwise returns + * false. + */ + private fun tryRegenerate( + projectPrebuiltsDir: File, + groupId: String, + artifactId: String, + version: Version, + location: ApiLocation, + ): Boolean { + if (File(projectPrebuiltsDir, version.toString()).exists()) { + regenerate(project.rootProject, groupId, artifactId, version, location) + return true + } + return false + } + + // Returns all (valid) artifact versions that appear to exist in

+ private fun listVersions(dir: File): List { + val pathNames: Array = dir.list() ?: arrayOf() + val files = pathNames.map { name -> File(dir, name) } + val subdirs = files.filter { child -> child.isDirectory() } + val versions = subdirs.map { child -> Version(child.name) } + val validVersions = versions.filter { v -> isValidArtifactVersion(v) } + return validVersions.sorted() + } + + private fun regenerate( + runnerProject: Project, + groupId: String, + artifactId: String, + version: Version, + outputApiLocation: ApiLocation, + ) { + val mavenId = "$groupId:$artifactId:$version" + val (compiledSources, sourceSets) = + try { + getFiles(runnerProject, mavenId) + } catch (e: TypedResolveException) { + runnerProject.logger.info("Ignoring missing artifact $mavenId: $e") + return + } + + if (outputApiLocation.publicApiFile.exists()) { + project.logger.lifecycle("Regenerating $mavenId") + val projectXml = File(temporaryDir, "$mavenId-project.xml") + ProjectXml.create( + sourceSets, + project.getAndroidJar().files, + compiledSources, + projectXml, + ) + generateApi( + project.getMetalavaClasspath(), + projectXml, + sourceSets.flatMap { it.sourcePaths.files }, + compiledSources = null, + outputApiLocation, + ApiLintMode.Skip, + generateRestrictToLibraryGroupAPIs, + emptyList(), + false, + kotlinSourceLevel.get(), + workerExecutor, + multiplatform = false, + ) + } else { + logger.warn("No API file for $mavenId") + } + } + + /** + * For the given [mavenId], returns a pair with the source jar as the first element, and + * [SourceSetInputs] representing the unzipped sources as the second element. + */ + private fun getFiles( + runnerProject: Project, + mavenId: String, + ): Pair> { + val jars = getJars(runnerProject, mavenId) + val sourcesMavenId = "$mavenId:sources" + val compiledSources = getCompiledSources(runnerProject, sourcesMavenId) + val sources = getSources(runnerProject, sourcesMavenId, compiledSources) + + // TODO(b/330721660) parse META-INF/kotlin-project-structure-metadata.json for KMP projects + // Represent the project as a single source set. + return compiledSources to + listOf( + SourceSetInputs( + // Since there's just one source set, the name is arbitrary. + sourceSetName = "main", + // There are no other source sets to depend on. + dependsOnSourceSets = emptyList(), + sourcePaths = sources, + dependencyClasspath = jars, + kotlinPlatforms = setOf(KotlinPlatformType.androidJvm), + ) + ) + } + + private fun getJars(runnerProject: Project, mavenId: String): FileCollection { + val configuration = + runnerProject.configurations.detachedConfiguration( + runnerProject.dependencies.create(mavenId) + ) + val resolvedConfiguration = configuration.resolvedConfiguration.resolvedArtifacts + val dependencyFiles = resolvedConfiguration.map { artifact -> artifact.file } + + val jars = dependencyFiles.filter { file -> file.name.endsWith(".jar") } + val aars = dependencyFiles.filter { file -> file.name.endsWith(".aar") } + val classesJars = + aars.map { aar -> + val tree = project.zipTree(aar) + val classesJar = + tree + .matching { filter: PatternFilterable -> filter.include("classes.jar") } + .single() + classesJar + } + val embeddedLibs = getEmbeddedLibs(runnerProject, mavenId) + val undeclaredJarDeps = getUndeclaredJarDeps(runnerProject, mavenId) + return runnerProject.files(jars + classesJars + embeddedLibs + undeclaredJarDeps) + } + + private fun getUndeclaredJarDeps(runnerProject: Project, mavenId: String): FileCollection { + if (mavenId.startsWith("androidx.wear:wear:")) { + return runnerProject.files("wear/wear_stubs/com.google.android.wearable-stubs.jar") + } + return runnerProject.files() + } + + /** Returns the source jar for the [mavenId]. */ + private fun getCompiledSources(runnerProject: Project, mavenId: String): File { + val configuration = + runnerProject.configurations.detachedConfiguration( + runnerProject.dependencies.create(mavenId) + ) + configuration.isTransitive = false + return configuration.singleFile + } + + /** Returns a file collection containing the unzipped sources from [compiledSources]. */ + private fun getSources( + runnerProject: Project, + mavenId: String, + compiledSources: File, + ): FileCollection { + val sanitizedMavenId = mavenId.replace(":", "-") + @Suppress("DEPRECATION") + val unzippedDir = File("${runnerProject.buildDir.path}/sources-unzipped/$sanitizedMavenId") + runnerProject.copy { copySpec -> + copySpec.from(runnerProject.zipTree(compiledSources)) + copySpec.into(unzippedDir) + } + return project.files(unzippedDir) + } + + private fun getEmbeddedLibs(runnerProject: Project, mavenId: String): Collection { + val configuration = + runnerProject.configurations.detachedConfiguration( + runnerProject.dependencies.create(mavenId) + ) + configuration.isTransitive = false + + val sanitizedMavenId = mavenId.replace(":", "-") + @Suppress("DEPRECATION") + val unzippedDir = File("${runnerProject.buildDir.path}/aars-unzipped/$sanitizedMavenId") + runnerProject.copy { copySpec -> + copySpec.from(runnerProject.zipTree(configuration.singleFile)) + copySpec.into(unzippedDir) + } + val libsDir = File(unzippedDir, "libs") + if (libsDir.exists()) { + return libsDir.listFiles()?.toList() ?: listOf() + } + + return listOf() + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateApiTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateApiTask.kt new file mode 100644 index 0000000000000..4a807bad8ec9d --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateApiTask.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.checkapi.ApiLocation +import com.google.common.io.Files +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.logging.Logger +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** + * Updates API signature text files. In practice, the values they will be updated to will match the + * APIs defined by the source code. + */ +@CacheableTask +abstract class UpdateApiTask : DefaultTask() { + + /** Text file from which API signatures will be read. */ + @get:Input abstract val inputApiLocation: Property + + /** Text files to which API signatures will be written. */ + @get:Internal // outputs are declared in getTaskOutputs() + abstract val outputApiLocations: ListProperty + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + fun getTaskInputs(): List { + val inputApi = inputApiLocation.get() + return listOf(inputApi.publicApiFile, inputApi.restrictedApiFile) + } + + @Suppress("unused") + @OutputFiles + fun getTaskOutputs(): List { + return outputApiLocations.get().flatMap { outputApiLocation -> + listOf(outputApiLocation.publicApiFile, outputApiLocation.restrictedApiFile) + } + } + + @TaskAction + fun exec() { + for (outputApi in outputApiLocations.get()) { + val inputApi = inputApiLocation.get() + copy(source = inputApi.publicApiFile, dest = outputApi.publicApiFile, logger = logger) + copy( + source = inputApi.restrictedApiFile, + dest = outputApi.restrictedApiFile, + logger = logger, + ) + } + } +} + +fun copy(source: File, dest: File, permitOverwriting: Boolean = true, logger: Logger? = null) { + if (!permitOverwriting) { + val sourceText = + if (source.exists()) { + source.readText() + } else { + "" + } + val overwriting = (dest.exists() && sourceText != dest.readText()) + val changing = overwriting || (dest.exists() != source.exists()) + if (changing) { + if (overwriting) { + val diff = summarizeDiff(source, dest, maxDiffLines + 1) + val diffMsg = + if (compareLineCount(diff, maxDiffLines) > 0) { + "Diff is greater than $maxDiffLines lines, use diff tool to compare.\n\n" + } else { + "Diff:\n$diff\n\n" + } + val message = + "Modifying the API definition for a previously released artifact " + + "having a final API version (version not ending in '-alpha') is not " + + "allowed.\n\n" + + "Previously declared definition is $dest\n" + + "Current generated definition is $source\n\n" + + diffMsg + + "Did you mean to increment the library version first?\n\n" + + "If you have a valid reason to override Semantic Versioning policy, see " + + "go/androidx/versioning#beta-api-change for information on obtaining " + + "approval." + throw GradleException(message) + } + } + } + + if (source.exists()) { + Files.copy(source, dest) + logger?.lifecycle("Wrote ${dest.name}") + } else if (dest.exists()) { + dest.delete() + logger?.lifecycle("Deleted ${dest.name}") + } +} + +/** + * Returns -1 if [text] has fewer than [count] newline characters, 0 if equal, and 1 if greater + * than. + */ +fun compareLineCount(text: String, count: Int): Int { + var found = 0 + var index = 0 + while (found < count) { + index = text.indexOf('\n', index) + if (index < 0) { + break + } + found++ + index++ + } + return if (found < count) { + -1 + } else if (found == count) { + 0 + } else { + 1 + } +} + +/** Maximum number of diff lines to include in output. */ +internal const val maxDiffLines = 8 diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateBaselineTasks.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateBaselineTasks.kt new file mode 100644 index 0000000000000..6f43d53523f7c --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/metalava/UpdateBaselineTasks.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import java.io.File +import javax.inject.Inject +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.TaskAction +import org.gradle.workers.WorkerExecutor + +@CacheableTask +internal abstract class UpdateApiLintBaselineTask +@Inject +constructor(workerExecutor: WorkerExecutor) : SourceMetalavaTask(workerExecutor) { + init { + group = "API" + description = + "Updates an API lint baseline file (api/api_lint.ignore) to match the " + + "current set of violations. Only use a baseline " + + "if you are in a library without Android dependencies, or when enabling a new " + + "lint check, and it is prohibitively expensive / not possible to fix the errors " + + "generated by enabling this lint check. " + } + + @OutputFile fun getOutputApiLintBaseline(): File = baselines.get().apiLintFile + + @TaskAction + fun updateBaseline() { + check(bootClasspath.files.isNotEmpty()) { "Android boot classpath not set." } + val baselineFile = baselines.get().apiLintFile + val checkArgs = + getGenerateApiArgs( + createProjectXmlFile(), + sourcePaths.files.filter { it.exists() }, + // API lint is not run on bytecode-only APIs, so don't bother processing the jar + // when generating a baseline. + compiledSources = null, + null, + GenerateApiMode.PublicApi, + ApiLintMode.CheckBaseline(baselineFile, targetsJavaConsumers.get()), + // API version history doesn't need to be generated + emptyList(), + manifestPath.orNull?.asFile?.absolutePath, + multiplatform.get(), + ) + val args = checkArgs + getCommonBaselineUpdateArgs(baselineFile) + + runWithArgs(args) + } +} + +@CacheableTask +internal abstract class IgnoreApiChangesTask @Inject constructor(workerExecutor: WorkerExecutor) : + CompatibilityMetalavaTask(workerExecutor) { + init { + description = + "Updates an API tracking baseline file (api/X.Y.Z.ignore) to match the " + + "current set of violations" + } + + // Declaring outputs prevents Gradle from rerunning this task if the inputs haven't changed + @OutputFiles + fun getTaskOutputs(): List? { + val apiBaselinesLocation = baselines.get() + return listOf(apiBaselinesLocation.publicApiFile, apiBaselinesLocation.restrictedApiFile) + } + + @TaskAction + fun exec() { + check(bootClasspath.files.isNotEmpty()) { "Android boot classpath not set." } + + val freezeApis = shouldFreezeApis(referenceApi.get().version(), version.get()) + updateBaseline(restricted = false, freezeApis) + if (restrictedApisExist()) { + updateBaseline(restricted = true, freezeApis) + } + } + + /** + * Updates the contents of the baseline file to specify an exception for every compatibility + * error found comparing the previous API to the current. + * + * @param restricted whether this compatibility check is for restricted APIs + * @param freezeApis whether APIs are frozen and no changes should be allowed + */ + private fun updateBaseline(restricted: Boolean, freezeApis: Boolean) { + val baseline = getBaselineFile(restricted) + val args = buildList { + addAll(getCommonBaselineUpdateArgs(baseline)) + addAll(getCompatibilityArguments(restricted, freezeApis)) + + add("--baseline") + add(baseline.toString()) + } + runWithArgs(args) + } +} + +private fun getCommonBaselineUpdateArgs(baselineFile: File): List { + // Create the baseline file if it does exist, as Metalava cannot handle non-existent files. + baselineFile.createNewFile() + return mutableListOf( + "--update-baseline", + baselineFile.toString(), + "--pass-baseline-updates", + "--delete-empty-baselines", + "--format=v4", + ) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/OWNERS b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/OWNERS new file mode 100644 index 0000000000000..3ea7bd5f0c7cd --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/OWNERS @@ -0,0 +1,2 @@ +dustinlam@google.com +rahulrav@google.com diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/ValidateIntegrationPatches.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/ValidateIntegrationPatches.kt new file mode 100644 index 0000000000000..754b62e48f3c4 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/ValidateIntegrationPatches.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.playground + +import androidx.build.addToBuildOnServer +import androidx.build.getSupportRootFolder +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import java.io.File +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import org.gradle.work.DisableCachingByDefault + +/** Validates that it is possible to apply the patch files in `.github/integration-patches`. */ +@DisableCachingByDefault(because = "Patch applies to all files, any change could break it") +abstract class ValidateIntegrationPatches : DefaultTask() { + @get:Inject abstract val execOperations: ExecOperations + + @get:[InputDirectory PathSensitive(PathSensitivity.NONE)] + abstract val patchesDirectory: DirectoryProperty + + @TaskAction + fun checkPatches() { + val patchFiles = patchesDirectory.asFileTree.files + for (patchFile in patchFiles) { + // Only check patch files, skip the README. + if (patchFile.extension == "patch") { + val result = + execOperations.exec { + it.commandLine( + "git", + "apply", + // This option will see if the patch can be applied but not apply it. + "--check", + patchFile.absolutePath, + ) + // Don't immediately error if the patch fails, to throw a custom error + // message. + it.isIgnoreExitValue = true + } + if (result.exitValue != 0) { + throw GradleException( + "Failed to apply patch file ${patchFile.absolutePath}\n" + + "See the instructions in $PATCH_DIRECTORY/README.md to fix it." + ) + } + } + } + } + + companion object { + private const val PATCH_DIRECTORY = ".github/integration-patches" + + fun createTask(project: Project) { + val task = + project.tasks.register( + "validateIntegrationPatches", + ValidateIntegrationPatches::class.java, + ) { task -> + task.patchesDirectory.set(File(project.getSupportRootFolder(), PATCH_DIRECTORY)) + task.cacheEvenIfNoOutputs() + } + project.addToBuildOnServer(task) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradleConfigurationTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradleConfigurationTask.kt new file mode 100644 index 0000000000000..27891a830e355 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/playground/VerifyPlaygroundGradleConfigurationTask.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.playground + +import com.google.common.annotations.VisibleForTesting +import java.io.File +import java.util.Properties +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider + +/** + * Compares the playground Gradle configuration with the main androidx Gradle configuration to + * ensure playgrounds do not define any property in their own build that conflicts with the main + * build. + */ +@CacheableTask +abstract class VerifyPlaygroundGradleConfigurationTask : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val androidxProperties: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val playgroundProperties: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val androidxGradleWrapper: RegularFileProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val playgroundGradleWrapper: RegularFileProperty + + @get:OutputFile abstract val outputFile: RegularFileProperty + + @TaskAction + fun checkPlaygroundGradleConfiguration() { + compareProperties() + compareGradleWrapperVersion() + // put the success into an output so that task can be up to date. + outputFile.get().asFile.writeText("valid", Charsets.UTF_8) + } + + private fun compareProperties() { + val rootProperties = loadPropertiesFile(androidxProperties.get().asFile) + val playgroundProperties = loadPropertiesFile(playgroundProperties.get().asFile) + validateProperties(rootProperties, playgroundProperties) + } + + private fun compareGradleWrapperVersion() { + val androidxGradleVersion = + readGradleVersionFromWrapperProperties(androidxGradleWrapper.get().asFile) + val playgroundGradleVersion = + readGradleVersionFromWrapperProperties(playgroundGradleWrapper.get().asFile) + if (androidxGradleVersion != playgroundGradleVersion) { + throw GradleException( + """ + Playground gradle version ($playgroundGradleVersion) must match the AndroidX main + build gradle version ($androidxGradleVersion). + """ + .trimIndent() + ) + } + } + + private fun readGradleVersionFromWrapperProperties(file: File): String { + val distributionUrl = loadPropertiesFile(file).getProperty("distributionUrl") + checkNotNull(distributionUrl) { + "cannot read distribution url from gradle wrapper file: ${file.canonicalPath}" + } + val gradleVersion = extractGradleVersion(distributionUrl) + return checkNotNull(gradleVersion) { + "Failed to extract gradle version from gradle wrapper file. Input: $distributionUrl" + } + } + + private fun validateProperties(rootProperties: Properties, playgroundProperties: Properties) { + // ensure we don't define properties that do not match the root file + // this includes properties that are not defined in the root androidx build as they might + // be properties which can alter the build output. We might consider allow listing certain + // properties in the future if necessary. + val propertyKeys = rootProperties.keys + playgroundProperties.keys + propertyKeys.forEach { key -> + val rootValue = rootProperties[key] + val playgroundValue = playgroundProperties[key] + + if ( + rootValue != playgroundValue && + !ignoredProperties.contains(key) && + exceptedProperties[key] != playgroundValue + ) { + throw GradleException( + """ + $key is defined in ${androidxProperties.get().asFile.absolutePath} as + $rootValue, which differs from $playgroundValue defined in + ${this.playgroundProperties.get().asFile.absolutePath}. If this change is + intentional, you can ignore it by adding it to ignoredProperties in + VerifyPlaygroundGradleConfigurationTask.kt + + Note: Having inconsistent properties in playground projects might trigger wrong + compilation output in the main AndroidX build, so if a property is defined in + playground properties, its value **MUST** match that of regular AndroidX build. + """ + .trimIndent() + ) + } + } + } + + private fun loadPropertiesFile(file: File) = + file.inputStream().use { inputStream -> Properties().apply { load(inputStream) } } + + companion object { + private const val TASK_NAME = "verifyPlaygroundGradleConfiguration" + + // A mapping of the expected override in playground, which should generally follow AOSP on + // androidx-main. Generally, should only be used for conflicting properties which have + // different values in different built targets on AOSP, but still should be declared in + // playground. + private val exceptedProperties = mapOf("androidx.writeVersionedApiFiles" to "true") + + private val ignoredProperties = + setOf( + "org.gradle.jvmargs", + "org.gradle.daemon", + "android.builder.sdkDownload", + "android.suppressUnsupportedCompileSdk", + "androidx.constraints", + ) + + /** + * Regular expression to extract the gradle version from a distributionUrl property. Sample + * input looks like: /gradle-7.3-rc-2-all.zip + */ + private val GRADLE_VERSION_REGEX = """/gradle-(.+)-(all|bin)\.zip$""".toRegex() + + @VisibleForTesting // make it accessible for buildSrc-tests + fun extractGradleVersion(distributionUrl: String): String? { + return GRADLE_VERSION_REGEX.find(distributionUrl)?.groupValues?.getOrNull(1) + } + + /** + * Creates the task to verify playground properties if an only if we have the + * playground-common folder to check against. + */ + fun createIfNecessary( + project: Project + ): TaskProvider? { + return if (project.projectDir.resolve("playground-common").exists()) { + project.tasks.register( + TASK_NAME, + VerifyPlaygroundGradleConfigurationTask::class.java, + ) { + it.androidxProperties.set( + project.layout.projectDirectory.file("gradle.properties") + ) + it.playgroundProperties.set( + project.layout.projectDirectory.file( + "playground-common/androidx-shared.properties" + ) + ) + it.androidxGradleWrapper.set( + project.layout.projectDirectory.file( + "gradle/wrapper/gradle-wrapper.properties" + ) + ) + it.playgroundGradleWrapper.set( + project.layout.projectDirectory.file( + "playground-common/gradle/wrapper/gradle-wrapper.properties" + ) + ) + it.outputFile.set( + project.layout.buildDirectory.file("playgroundPropertiesValidation.out") + ) + } + } else { + null + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CheckResourceApiReleaseTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CheckResourceApiReleaseTask.kt new file mode 100644 index 0000000000000..683e5979400ca --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CheckResourceApiReleaseTask.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.resources + +import androidx.build.checkapi.ApiLocation +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** Task for verifying changes in the public Android resource surface, e.g. `public.xml`. */ +@CacheableTask +abstract class CheckResourceApiReleaseTask : DefaultTask() { + /** Reference resource API file (in source control). */ + @get:InputFiles // InputFiles allows non-existent files, whereas InputFile does not. + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val referenceApiFile: RegularFileProperty + + /** Generated resource API file (in build output). */ + @get:Internal abstract val apiLocation: Property + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + fun getTaskInput(): File { + return apiLocation.get().resourceFile + } + + @TaskAction + fun checkResourceApiRelease() { + val referenceApiFile = referenceApiFile.get().asFile + val apiFile = apiLocation.get().resourceFile + + // Read the current API surface, if any, into memory. + val newApiSet = + if (apiFile.exists()) { + apiFile.readLines().toSet() + } else { + emptySet() + } + + // Read the reference API surface into memory. + val referenceApiSet = + if (referenceApiFile.exists()) { + referenceApiFile.readLines().toSet() + } else { + emptySet() + } + + // POLICY: Ensure that no resources are removed from the last released version. + val removedApiSet = referenceApiSet - newApiSet + if (removedApiSet.isNotEmpty()) { + var removed = "" + for (e in removedApiSet) { + removed += "$e\n" + } + + val errorMessage = + """Public resources have been removed since the previous revision + +Previous definition is ${referenceApiFile.canonicalPath} +Current definition is ${apiFile.canonicalPath} + +Public resources are considered part of the library's API surface +and may not be removed within a major version. + +Removed resources: +$removed""" + + throw GradleException(errorMessage) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CheckResourceApiTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CheckResourceApiTask.kt new file mode 100644 index 0000000000000..c0f1627d2c115 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CheckResourceApiTask.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.resources + +import androidx.build.checkapi.ApiLocation +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** Task for detecting changes in the public Android resource surface, e.g. `public.xml`. */ +@CacheableTask +abstract class CheckResourceApiTask : DefaultTask() { + /** Checked in resource API files (in source control). */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val checkedInApiFiles: ListProperty + + /** Generated resource API file (in build output). */ + @get:Internal abstract val apiLocation: Property + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + fun getTaskInput(): File { + return apiLocation.get().resourceFile + } + + @TaskAction + fun checkResourceApi() { + val builtApi = apiLocation.get().resourceFile + + for (checkedInApi in checkedInApiFiles.get()) { + androidx.build.metalava.checkEqual(checkedInApi, builtApi) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CopyPublicResourcesDirTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CopyPublicResourcesDirTask.kt new file mode 100644 index 0000000000000..9609c3fbfd821 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/CopyPublicResourcesDirTask.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.resources + +import java.io.File +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** Copy task that adds a [DirectoryProperty] to be used in variant.addGeneratedSourceDirectory() */ +@DisableCachingByDefault( + because = " Copy tasks are faster to rerun locally than to fetch from the remote cache." +) +abstract class CopyPublicResourcesDirTask : DefaultTask() { + + @get:Inject abstract val fileSystemOperations: FileSystemOperations + + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val buildSrcResDir: DirectoryProperty + + @get:OutputDirectory abstract val outputFolder: DirectoryProperty + + @TaskAction + fun copy() { + File(outputFolder.get().asFile.path).apply { + deleteRecursively() + mkdirs() + fileSystemOperations.copy { + it.from(buildSrcResDir) + it.into(this) + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/GenerateResourceApiTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/GenerateResourceApiTask.kt new file mode 100644 index 0000000000000..09601c64f905a --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/GenerateResourceApiTask.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.resources + +import androidx.build.checkapi.ApiLocation +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** Generates a resource API file for consumption by other API tasks. */ +@CacheableTask +abstract class GenerateResourceApiTask : DefaultTask() { + /** + * Public resources text file generated by AAPT. + * + * This file must be defined, but the file may not exist on the filesystem if the library has no + * resources. In that case, we will generate an empty API signature file. + */ + @get:InputFiles // InputFiles allows non-existent files, whereas InputFile does not. + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val builtApi: RegularFileProperty + + /** Source files against which API signatures will be validated. */ + @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)] + var sourcePaths: Collection = emptyList() + + /** Text file to which API signatures will be written. */ + @get:Internal abstract val apiLocation: Property + + @OutputFile + fun getTaskOutput(): File { + return apiLocation.get().resourceFile + } + + @TaskAction + fun generateResourceApi() { + val builtApiFile = builtApi.get().asFile + val sortedApiLines = + if (builtApiFile.exists()) { + builtApiFile.readLines().toSortedSet() + } else { + val errorMessage = + """No public resources defined + +At least one public resource must be defined to prevent all resources from +appearing public by default. + +This exception should never occur for AndroidX projects, as a +resource is added by default to all library project. Please contact the +AndroidX Core team for assistance.""" + throw GradleException(errorMessage) + } + + val outputApiFile = apiLocation.get().resourceFile + outputApiFile.bufferedWriter().use { out -> + sortedApiLines.forEach { + out.write(it) + out.newLine() + } + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt new file mode 100644 index 0000000000000..61de37f735348 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/PublicResourcesStubHelper.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.resources + +import androidx.build.getSupportRootFolder +import com.android.build.api.variant.LibraryVariant +import java.io.File +import org.gradle.api.Project + +fun Project.configurePublicResourcesStub(libraryVariant: LibraryVariant) { + val copyPublicResourcesDirTask = + tasks.register("generatePublicResourcesStub", CopyPublicResourcesDirTask::class.java) { task + -> + task.buildSrcResDir.set(File(getSupportRootFolder(), "buildSrc/res")) + } + libraryVariant.sources.res?.addGeneratedSourceDirectory( + copyPublicResourcesDirTask, + CopyPublicResourcesDirTask::outputFolder, + ) +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/ResourceTasks.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/ResourceTasks.kt new file mode 100644 index 0000000000000..c3127ebca8223 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/ResourceTasks.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.resources + +import androidx.build.AndroidXImplPlugin.Companion.TASK_GROUP_API +import androidx.build.addToBuildOnServer +import androidx.build.addToCheckTask +import androidx.build.checkapi.ApiLocation +import androidx.build.checkapi.getRequiredCompatibilityApiLocation +import androidx.build.metalava.UpdateApiTask +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import org.gradle.api.Project +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.Provider + +object ResourceTasks { + private const val GENERATE_RESOURCE_API_TASK = "generateResourceApi" + private const val CHECK_RESOURCE_API_RELEASE_TASK = "checkResourceApiRelease" + private const val CHECK_RESOURCE_API_TASK = "checkResourceApi" + private const val UPDATE_RESOURCE_API_TASK = "updateResourceApi" + + fun setupProject( + project: Project, + builtApiFile: Provider, + builtApiLocation: ApiLocation, + outputApiLocations: List, + ) { + + val outputApiFiles = outputApiLocations.map { location -> location.resourceFile } + + val generateResourceApi = + project.tasks.register( + GENERATE_RESOURCE_API_TASK, + GenerateResourceApiTask::class.java, + ) { task -> + task.group = "API" + task.description = "Generates resource API files from source" + task.builtApi.set(builtApiFile) + task.apiLocation.set(builtApiLocation) + } + + // Policy: If the artifact has previously been released, e.g. has a beta or later API file + // checked in, then we must verify "release compatibility" against the work-in-progress + // API file. + val checkResourceApiRelease = + project.getRequiredCompatibilityApiLocation()?.let { lastReleasedApiFile -> + project.tasks.register( + CHECK_RESOURCE_API_RELEASE_TASK, + CheckResourceApiReleaseTask::class.java, + ) { task -> + task.referenceApiFile.set(lastReleasedApiFile.resourceFile) + task.apiLocation.set(generateResourceApi.flatMap { it.apiLocation }) + // Since apiLocation isn't a File, we have to manually set up the dependency. + task.dependsOn(generateResourceApi) + task.cacheEvenIfNoOutputs() + } + } + + // Policy: All changes to API surfaces for which compatibility is enforced must be + // explicitly confirmed by running the updateApi task. To enforce this, the implementation + // checks the "work-in-progress" built API file against the checked in current API file. + val checkResourceApi = + project.tasks.register(CHECK_RESOURCE_API_TASK, CheckResourceApiTask::class.java) { task + -> + task.group = TASK_GROUP_API + task.description = + "Checks that the resource API generated from source matches the " + + "checked in resource API file" + task.apiLocation.set(generateResourceApi.flatMap { it.apiLocation }) + // Since apiLocation isn't a File, we have to manually set up the dependency. + task.dependsOn(generateResourceApi) + task.cacheEvenIfNoOutputs() + task.checkedInApiFiles.set(outputApiFiles) + checkResourceApiRelease?.let { task.dependsOn(it) } + } + + val updateResourceApi = + project.tasks.register(UPDATE_RESOURCE_API_TASK, UpdateResourceApiTask::class.java) { + task -> + task.group = TASK_GROUP_API + task.description = + "Updates the checked in resource API files to match source code API" + task.apiLocation.set(generateResourceApi.flatMap { it.apiLocation }) + // Since apiLocation isn't a File, we have to manually set up the dependency. + task.dependsOn(generateResourceApi) + task.outputApiLocations.set(outputApiLocations) + task.forceUpdate.set(project.providers.gradleProperty("force").isPresent) + checkResourceApiRelease?.let { + // If a developer (accidentally) makes a non-backwards compatible change to an + // API, the developer will want to be informed of it as soon as possible. + // So, whenever a developer updates an API, if backwards compatibility checks + // are + // enabled in the library, then we want to check that the changes are backwards + // compatible + task.dependsOn(it) + } + } + + // Ensure that this task runs as part of "updateApi" task from MetalavaTasks. + project.tasks.withType(UpdateApiTask::class.java).configureEach { task -> + task.dependsOn(updateResourceApi) + } + + project.addToCheckTask(checkResourceApi) + project.addToBuildOnServer(checkResourceApi) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/UpdateResourceApiTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/UpdateResourceApiTask.kt new file mode 100644 index 0000000000000..a1355ed274ca5 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/resources/UpdateResourceApiTask.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.resources + +import androidx.build.checkapi.ApiLocation +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +/** Task for updating the public Android resource surface, e.g. `public.xml`. */ +@CacheableTask +abstract class UpdateResourceApiTask : DefaultTask() { + /** Generated resource API file (in build output). */ + @get:Internal abstract val apiLocation: Property + + @get:Input abstract val forceUpdate: Property + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + fun getTaskInput(): File { + return apiLocation.get().resourceFile + } + + /** Resource API files to which APIs should be written (in source control). */ + @get:Internal // outputs are declared in getTaskOutputs() + abstract val outputApiLocations: ListProperty + + @OutputFiles + fun getTaskOutputs(): List { + return outputApiLocations.get().flatMap { outputApiLocation -> + listOf(outputApiLocation.resourceFile) + } + } + + @TaskAction + fun updateResourceApi() { + var permitOverwriting = true + for (outputApi in outputApiLocations.get()) { + val version = outputApi.version() + if ( + version != null && + version.isFinalApi() && + outputApi.publicApiFile.exists() && + !forceUpdate.get() + ) { + permitOverwriting = false + } + } + + val inputApi = apiLocation.get().resourceFile + + for (outputApi in outputApiLocations.get()) { + androidx.build.metalava.copy( + inputApi, + outputApi.resourceFile, + permitOverwriting, + logger, + ) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sbom/ExportSbomsTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sbom/ExportSbomsTask.kt new file mode 100644 index 0000000000000..8750c65fd98f5 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sbom/ExportSbomsTask.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.sbom + +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault + +/** Copies the project's SBOM file to the distribution directory. */ +@DisableCachingByDefault(because = "Zip tasks are not worth caching according to Gradle") +abstract class ExportSbomsTask : DefaultTask() { + @get:Inject abstract val fileSystemOperations: FileSystemOperations + + @get:OutputDirectory abstract val destinationDir: DirectoryProperty + + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sbomFile: RegularFileProperty + + @get:Input abstract val outputFileName: Property + + @TaskAction + fun copySboms() { + if (!sbomFile.get().asFile.exists()) { + throw GradleException("sbom file does not exist: ${sbomFile.get().asFile.path}") + } + destinationDir.get().asFile.mkdirs() + fileSystemOperations.copy { + it.from(sbomFile) + it.into(destinationDir) + it.rename(sbomFile.get().asFile.name, outputFileName.get()) + } + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt new file mode 100644 index 0000000000000..666d3520bcfa7 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt @@ -0,0 +1,351 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.sbom + +import androidx.build.AndroidXPlaygroundRootImplPlugin +import androidx.build.BundleInsideHelper +import androidx.build.ProjectLayoutType +import androidx.build.addSbomToAttestation +import androidx.build.addToBuildOnServer +import androidx.build.getDistributionDirectory +import androidx.build.getPrebuiltsRoot +import androidx.build.getSupportRootFolder +import androidx.build.gitclient.getHeadShaProvider +import androidx.inspection.gradle.EXPORT_INSPECTOR_DEPENDENCIES +import androidx.inspection.gradle.IMPORT_INSPECTOR_DEPENDENCIES +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import java.io.File +import java.net.URI +import java.util.UUID +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.artifacts.ModuleVersionIdentifier +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.bundling.AbstractArchiveTask +import org.gradle.api.tasks.bundling.Zip +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.getByType +import org.spdx.sbom.gradle.SpdxSbomExtension +import org.spdx.sbom.gradle.SpdxSbomTask +import org.spdx.sbom.gradle.extensions.DefaultSpdxSbomTaskExtension +import org.spdx.sbom.gradle.project.ProjectInfo +import org.spdx.sbom.gradle.project.ScmInfo + +/** + * Tells whether the contents of the Configuration with the given name should be listed in our sbom + * + * That is, this tells whether the corresponding Configuration contains dependencies that get + * embedded into our build artifact + */ +private fun Project.shouldSbomIncludeConfigurationName(configurationName: String): Boolean { + return when (configurationName) { + BundleInsideHelper.CONFIGURATION_NAME -> true + "shadowed" -> true + // compileClasspath is included by the Shadow plugin by default but projects that + // declare a "shadowed" configuration exclude the "compileClasspath" configuration from + // the shadowJar task + "compileClasspath" -> appliesShadowPlugin() && configurations.findByName("shadowed") == null + EXPORT_INSPECTOR_DEPENDENCIES -> true + IMPORT_INSPECTOR_DEPENDENCIES -> true + // https://github.com/spdx/spdx-gradle-plugin/issues/12 + sbomEmptyConfiguration -> true + else -> false + } +} + +// An empty Configuration for the sbom plugin to ensure it has at least one Configuration +private const val sbomEmptyConfiguration = "sbomEmpty" + +// some tasks that don't embed configurations having external dependencies +private val excludeTaskNames = + setOf( + "distZip", + "shadowDistZip", + "annotationsZip", + "protoLiteJar", + "bundleDebugLocalLintAar", + "bundleReleaseLocalLintAar", + "bundleDebugAar", + "bundleReleaseAar", + "bundleAndroidMainAar", + "bundleAndroidMainLocalLintAar", + "repackageAndroidMainAar", + "repackageAarWithResourceApiAndroidMain", + ) + +/** + * Lists the Configurations that we should declare we're embedding into the output of this task + * + * The immediate inputs to the task are not generally mentioned here: external entities aren't + * interested in knowing that our .aar file contains a classes.jar + * + * The external dependencies that embed into our artifacts are what we mention here: external + * entities might be interested in knowing if, for example, we embed protobuf-javalite into our + * artifact + * + * The purpose of this function is to detect new archive tasks and remind developers to update + * shouldSbomIncludeConfigurationName + */ +private fun Project.listSbomConfigurationNamesForArchive(task: AbstractArchiveTask): List { + if (task is Jar && task !is ShadowJar) { + // Jar tasks don't generally embed other dependencies in them + return listOf() + } + if (task is Zip && task.name.endsWith("Klib")) { + // klib zip tasks don't generally embed other dependencies in them + return listOf() + } + + val projectPath = path + val taskName = task.name + + // some tasks that embed other configurations + if (taskName == BundleInsideHelper.REPACKAGE_TASK_NAME) { + return listOf(BundleInsideHelper.CONFIGURATION_NAME) + } + if ( + projectPath.contains("inspection") && + (taskName == "assembleInspectorJarRelease" || + taskName == "inspectionShadowDependenciesRelease") + ) { + return listOf(EXPORT_INSPECTOR_DEPENDENCIES) + } + + if (excludeTaskNames.contains(taskName)) return listOf() + if (projectPath == ":compose:lint:internal-lint-checks") + return listOf() // we don't publish these lint checks + if (projectPath.contains("integration-tests")) + return listOf() // we don't publish integration tests + if (taskName.startsWith("zip") && taskName.contains("ResultsOf") && taskName.contains("Test")) + return listOf() // we don't publish test results + + // ShadowJar tasks have a `configurations` property that lists the configurations that + // are inputs to the task, but they don't also list file inputs + // If a project only has one shadowJar task (named "shadowJar"), for now we assume + // that it doesn't include any external files that aren't already declared in + // its configurations. + // If a project has multiple shadowJar tasks, we ask the developer to provide + // this metadata somehow by failing below + if (taskName == "shadowJar" || taskName == "shadowLibraryJar") { + // If the task is a ShadowJar task, we can just ask it which configurations it intends to + // embed + // We separately validate that this list is correct in + val shadowTask = task as? ShadowJar + if (shadowTask != null) { + val configurations = + configurations.filter { conf -> shadowTask.configurations.contains(conf) } + return configurations.map { conf -> conf.name } + } + } + + if (taskName == "stubAar") { + return listOf() + } + + throw GradleException( + "Not sure which external dependencies are included in $projectPath:$taskName of type " + + "${task::class.java} (this is used for publishing sboms). Please update " + + "Sbom.kt's listSbomConfigurationNamesForArchive and " + + "shouldSbomIncludeConfigurationName" + ) +} + +/** Validates that the inputs of the given archive task are recognized */ +private fun Project.validateArchiveInputsRecognized(task: AbstractArchiveTask) { + val configurationNames = listSbomConfigurationNamesForArchive(task) + for (configurationName in configurationNames) { + if (!shouldSbomIncludeConfigurationName(configurationName)) { + throw GradleException( + "Task listSbomConfigurationNamesForArchive(\"${task.name}\") = " + + "$configurationNames but " + + "shouldSbomIncludeConfigurationName(\"$configurationName\") = false. " + + "You probably should update shouldSbomIncludeConfigurationName to match" + ) + } + } +} + +/** Validates that the inputs of each archive task are recognized */ +fun Project.validateAllArchiveInputsRecognized() { + tasks.withType(Zip::class.java).configureEach { task -> validateArchiveInputsRecognized(task) } + tasks.withType(ShadowJar::class.java).configureEach { task -> + validateArchiveInputsRecognized(task) + } +} + +/** Enables the publishing of an sbom that lists our embedded dependencies */ +fun Project.configureSbomPublishing(isolatedProjectsEnabled: Boolean) { + val uuid = coordinatesToUUID().toString() + val projectName = name + val projectVersion = version.toString() + + configurations.create(sbomEmptyConfiguration) { emptyConfiguration -> + emptyConfiguration.isCanBeConsumed = false + } + apply(plugin = "org.spdx.sbom") + val repos = getRepoPublicUrls() + val headShaProvider = getHeadShaProvider() + val supportRootDir = getSupportRootFolder() + + val sbomBuiltFile = layout.buildDirectory.file("spdx/release.spdx.json") + + val publishTask = + tasks.register("exportSboms", ExportSbomsTask::class.java) { publishTask -> + publishTask.destinationDir.set(getSbomPublishDir()) + publishTask.sbomFile.set(sbomBuiltFile) + publishTask.outputFileName.set("$projectName-$projectVersion.spdx.json") + } + + if (!isolatedProjectsEnabled) { + addSbomToAttestation( + publishTask.map { task -> + task.destinationDir + .file(task.outputFileName.get()) + .get() + .asFile + .toRelativeString(getDistributionDirectory().get().asFile) + } + ) + } + + tasks.withType(SpdxSbomTask::class.java).configureEach { task -> + val sbomProjectDir = projectDir + + task.taskExtension.set( + object : DefaultSpdxSbomTaskExtension() { + override fun mapRepoUri(repoUri: URI?, artifact: ModuleVersionIdentifier): URI { + val uriString = repoUri.toString() + for (repo in repos) { + val ourRepoUrl = repo.key + val publicRepoUrl = repo.value + if (uriString.startsWith(ourRepoUrl)) { + return URI.create(publicRepoUrl) + } + if (System.getenv("ALLOW_PUBLIC_REPOS") != null) { + if (uriString.startsWith(publicRepoUrl)) { + return URI.create(publicRepoUrl) + } + } + } + throw GradleException( + "Cannot determine public repo url for repo $uriString artifact $artifact" + ) + } + + override fun mapScmForProject( + original: ScmInfo, + projectInfo: ProjectInfo, + ): ScmInfo { + val url = getGitRemoteUrl(projectInfo.projectDirectory, supportRootDir) + return ScmInfo.from("git", url, headShaProvider.get()) + } + + override fun shouldCreatePackageForProject(projectInfo: ProjectInfo): Boolean { + // sbom should include the project it describes + if (sbomProjectDir.equals(projectInfo.projectDirectory)) return true + // sbom doesn't need to list our projects as dependencies; + // they're implementation details + // Example: glance:glance-appwidget uses glance:glance-appwidget-proto + if (pathContains(supportRootDir, projectInfo.projectDirectory)) return false + // sbom should list remaining project dependencies + return true + } + } + ) + } + + val sbomExtension = extensions.getByType() + val sbomConfigurations = mutableListOf() + + afterEvaluate { + configurations.configureEach { configuration -> + if (shouldSbomIncludeConfigurationName(configuration.name)) { + sbomConfigurations.add(configuration.name) + } + } + + sbomExtension.targets.create("release") { target -> + val googleOrganization = "Organization: Google LLC" + val document = target.document + document.namespace.set("https://spdx.google.com/$uuid") + document.creator.set(googleOrganization) + document.packageSupplier.set(googleOrganization) + + target.configurations.set(sbomConfigurations) + } + addToBuildOnServer(tasks.named("spdxSbomForRelease")) + publishTask.configure { task -> task.dependsOn("spdxSbomForRelease") } + } +} + +// Returns a UUID whose contents are based on the project's coordinates (group:artifact:version) +private fun Project.coordinatesToUUID(): UUID { + val coordinates = "$group:$name:$version" + val bytes = coordinates.toByteArray() + return UUID.nameUUIDFromBytes(bytes) +} + +private fun pathContains(ancestor: File, child: File): Boolean { + val childNormalized = child.getCanonicalPath() + File.separator + val ancestorNormalized = ancestor.getCanonicalPath() + File.separator + return childNormalized.startsWith(ancestorNormalized) +} + +private fun getGitRemoteUrl(dir: File, supportRootDir: File): String { + if (pathContains(supportRootDir, dir)) { + return "android.googlesource.com/platform/frameworks/support" + } + + val notoFontsDir = File("$supportRootDir/../../external/noto-fonts") + if (pathContains(notoFontsDir, dir)) { + return "android.googlesource.com/platform/external/noto-fonts" + } + + val icingDir = File("$supportRootDir/../../external/icing") + if (pathContains(icingDir, dir)) { + return "android.googlesource.com/platform/external/icing" + } + throw GradleException("Could not identify git remote url for project at $dir") +} + +private fun Project.getSbomPublishDir(): Provider { + val groupPath = group.toString().replace(".", "/") + val fullPath = "sboms/$groupPath/$name/$version" + return getDistributionDirectory().dir(fullPath) +} + +private const val MAVEN_CENTRAL_REPO_URL = "https://repo.maven.apache.org/maven2" +private const val GMAVEN_REPO_URL = "https://dl.google.com/android/maven2" + +/** Returns a mapping from local repo url to public repo url */ +private fun Project.getRepoPublicUrls(): Map { + return if (ProjectLayoutType.isPlayground(this)) { + mapOf( + MAVEN_CENTRAL_REPO_URL to MAVEN_CENTRAL_REPO_URL, + AndroidXPlaygroundRootImplPlugin.INTERNAL_PREBUILTS_REPO_URL to GMAVEN_REPO_URL, + ) + } else { + mapOf( + "file:${getPrebuiltsRoot()}/androidx/external" to MAVEN_CENTRAL_REPO_URL, + "file:${getPrebuiltsRoot()}/androidx/internal" to GMAVEN_REPO_URL, + ) + } +} + +private fun Project.appliesShadowPlugin() = pluginManager.hasPlugin("com.gradleup.shadow") diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sources/SourceJarTaskHelper.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sources/SourceJarTaskHelper.kt new file mode 100644 index 0000000000000..11d0fc9b7cf7a --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sources/SourceJarTaskHelper.kt @@ -0,0 +1,347 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.sources + +import androidx.build.LazyInputsCopyTask +import androidx.build.ProjectLayoutType +import androidx.build.ProjectLayoutType.Companion.isJetBrainsFork +import androidx.build.capitalize +import androidx.build.dackka.DokkaAnalysisPlatform +import androidx.build.dackka.docsPlatform +import androidx.build.multiplatformExtension +import androidx.build.registerAsComponentForKmpPublishing +import androidx.build.registerAsComponentForPublishing +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.api.variant.LibraryVariant +import com.google.gson.GsonBuilder +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.DocsType +import org.gradle.api.attributes.Usage +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.bundling.Jar +import org.gradle.kotlin.dsl.named +import org.jetbrains.androidx.build.JetBrainsPublication +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget + +/** Sets up a source jar task for an Android library project. */ +fun Project.configureSourceJarForAndroid( + libraryVariant: LibraryVariant, + samplesProjects: MutableCollection, +) { + if (isJetBrainsFork(project)) return + val allSources = + project.files(libraryVariant.sources.java?.all) + + project.files(libraryVariant.sources.kotlin?.all) + val sourceJar = + tasks.register("sourceJar${libraryVariant.name.capitalize()}", Jar::class.java) { task -> + task.archiveClassifier.set("sources") + task.from(allSources) + task.exclude { it.file.path.contains("generated") } + // Do not allow source files with duplicate names, information would be lost + // otherwise. + task.duplicatesStrategy = DuplicatesStrategy.FAIL + } + registerSourcesVariant(sourceJar) + + val publishingVariants = + project.multiplatformExtension?.let { + listOf( + PublishingVariant.AgpLibrarySourcesElements, + PublishingVariant.KmpSourcesElements, + ) + } ?: listOf(PublishingVariant.SourcesElements) + + registerSamplesLibraries(samplesProjects, publishingVariants) + + configurations.whenObjectAdded { + if (it.name == "releaseSourcesElements") { + it.isCanBeConsumed = false + } + } + + val disableNames = setOf("releaseSourcesJar") + disableUnusedSourceJarTasks(disableNames) +} + +fun Project.configureMultiplatformSourcesForAndroid(samplesProjects: MutableCollection) { + if (isJetBrainsFork(project)) return + registerSamplesLibraries( + samplesProjects, + listOf(PublishingVariant.KmpSourcesElements, PublishingVariant.AgpKmpSourcesElements), + ) +} + +/** Sets up a source jar task for a Java library project. */ +fun Project.configureSourceJarForJava(samplesProjects: MutableCollection) { + if (isJetBrainsFork(project)) return + val sourceJar = + tasks.register("sourceJar", Jar::class.java) { task -> + task.archiveClassifier.set("sources") + + // Do not allow source files with duplicate names, information would be lost otherwise. + // Different sourceSets in KMP should use different platform infixes, see b/203764756 + task.duplicatesStrategy = DuplicatesStrategy.FAIL + + extensions.findByType(JavaPluginExtension::class.java)?.let { javaExtension -> + // Since KotlinPlugin applies JavaPlugin, it's possible for JavaPlugin to exist, but + // not to have "main". Eventually, we should stop expecting to grab sourceSets by + // name + // (b/235828421) + javaExtension.sourceSets.findByName("main")?.let { + task.from(it.allSource.sourceDirectories) + } + } + + extensions.findByType(KotlinMultiplatformExtension::class.java)?.let { kmpExtension -> + for (sourceSetName in listOf("commonMain", "jvmMain")) { + kmpExtension.sourceSets.findByName(sourceSetName)?.let { sourceSet -> + task.from(sourceSet.kotlin.sourceDirectories) + } + } + } + } + registerSourcesVariant(sourceJar) + registerSamplesLibraries(samplesProjects, listOf(PublishingVariant.SourcesElements)) + + val disableNames = setOf("kotlinSourcesJar") + disableUnusedSourceJarTasks(disableNames) +} + +fun Project.configureSourceJarForMultiplatform() { + if (isJetBrainsFork(project) && JetBrainsPublication.shouldPublish(this)) return + val kmpExtension = + multiplatformExtension + ?: throw GradleException( + "Unable to find multiplatform extension while configuring multiplatform source JAR" + ) + val metadataFile = layout.buildDirectory.file(PROJECT_STRUCTURE_METADATA_FILEPATH) + val multiplatformMetadataTask = + tasks.register("createMultiplatformMetadata", CreateMultiplatformMetadata::class.java) { + it.metadataFile.set(metadataFile) + it.sourceSetMetadata = project.provider { createSourceSetMetadata(kmpExtension) } + } + val sourceJar = + tasks.register("multiplatformSourceJar", Jar::class.java) { task -> + task.dependsOn(multiplatformMetadataTask) + task.archiveClassifier.set("multiplatform-sources") + + // Do not allow source files with duplicate names, information would be lost otherwise. + // Different sourceSets in KMP should use different platform infixes, see b/203764756 + task.duplicatesStrategy = DuplicatesStrategy.FAIL + kmpExtension.targets + // Filter out sources from stub targets as they are not intended to be documented + .filterNot { it.name in setOfStubTargets } + .flatMap { it.mainCompilation().allKotlinSourceSets } + .toSet() + // Sort sourceSets to ensure child sourceSets come after their parents, b/404784813 + .sortedWith(compareBy({ it.dependsOn.size }, { it.name })) + .forEach { sourceSet -> + task.from(sourceSet.kotlin.srcDirs) { copySpec -> + copySpec.into(sourceSet.name) + } + } + task.metaInf.from(metadataFile) + } + registerMultiplatformSourcesVariant(sourceJar) + + val disableNames = setOf("kotlinSourcesJar") + disableUnusedSourceJarTasks(disableNames) +} + +fun Project.disableUnusedSourceJarTasks(disableNames: Set) { + project.tasks.configureEach { task -> + if (disableNames.contains(task.name)) { + task.enabled = false + } + } +} + +internal val Project.multiplatformUsage + get() = objects.named("androidx-multiplatform-docs") + +private fun Project.registerMultiplatformSourcesVariant(sourceJar: TaskProvider) = + registerSourcesVariant(PublishingVariant.KmpSourcesElements.name, sourceJar, multiplatformUsage) + .also { registerAsComponentForKmpPublishing(it) } + +private fun Project.registerSourcesVariant(sourceJar: TaskProvider) = + registerSourcesVariant( + PublishingVariant.SourcesElements.name, + sourceJar, + objects.named(Usage.JAVA_RUNTIME), + ) + +private fun Project.registerSourcesVariant( + configurationName: String, + sourceJar: TaskProvider, + usage: Usage, +) = + configurations.create(configurationName) { gradleVariant -> + gradleVariant.isCanBeResolved = false + gradleVariant.attributes.attribute(Usage.USAGE_ATTRIBUTE, usage) + gradleVariant.attributes.attribute( + Category.CATEGORY_ATTRIBUTE, + objects.named(Category.DOCUMENTATION), + ) + gradleVariant.attributes.attribute( + Bundling.BUNDLING_ATTRIBUTE, + objects.named(Bundling.EXTERNAL), + ) + gradleVariant.attributes.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + objects.named(DocsType.SOURCES), + ) + gradleVariant.outgoing.artifact(sourceJar) + registerAsComponentForPublishing(gradleVariant) + } + +/** + * Finds the main compilation for a source set, usually called 'main' but for android we need to + * search for 'release' instead. + */ +private fun KotlinTarget.mainCompilation() = + compilations.findByName(MAIN_COMPILATION_NAME) ?: compilations.getByName("release") + +/** + * Writes a metadata file to the given [metadataFile] location for all multiplatform Kotlin source + * sets including their dependencies and analysisPlatform. This is consumed when we are reading + * source JARs so that we can pass the correct inputs to Dackka. + */ +@CacheableTask +abstract class CreateMultiplatformMetadata : DefaultTask() { + @Input lateinit var sourceSetMetadata: Provider> + + @get:OutputFile abstract val metadataFile: RegularFileProperty + + @TaskAction + fun execute() { + metadataFile.get().asFile.apply { + parentFile.mkdirs() + createNewFile() + val gson = GsonBuilder().setPrettyPrinting().create() + writeText(gson.toJson(sourceSetMetadata.get())) + } + } +} + +fun createSourceSetMetadata(kmpExtension: KotlinMultiplatformExtension): Map { + val commonMain = kmpExtension.sourceSets.getByName("commonMain") + val sourceSetsByName = + mutableMapOf( + "commonMain" to + mapOf( + "name" to commonMain.name, + "dependencies" to commonMain.dependsOn.map { it.name }.sorted(), + "analysisPlatform" to DokkaAnalysisPlatform.COMMON.jsonName, + ) + ) + kmpExtension.targets.forEach { target -> + // Skip adding entries for stub targets are they are not intended to be documented + if (target.name in setOfStubTargets) return@forEach + target.mainCompilation().allKotlinSourceSets.forEach { + sourceSetsByName.getOrPut(it.name) { + mapOf( + "name" to it.name, + "dependencies" to it.transitiveDependsOn().map { it.name }.sorted(), + "analysisPlatform" to target.docsPlatform().jsonName, + ) + } + } + } + return mapOf("sourceSets" to sourceSetsByName.keys.sorted().map { sourceSetsByName[it] }) +} + +private fun KotlinSourceSet.transitiveDependsOn(): Set { + val directDependencies = this.dependsOn + return directDependencies + directDependencies.flatMap { it.transitiveDependsOn() } +} + +private fun Project.registerSamplesLibraries( + samplesProjects: MutableCollection, + publishingVariants: List, +) = + samplesProjects.forEach { sampleProject -> + dependencies.add("samples", sampleProject) + updateCopySampleSourceJarsTaskWithVariant(publishingVariants.map { it.name }) + } + +/** + * Updates the published variants with the output of [LazyInputsCopyTask]. This function must be + * called in the stack of [LibraryAndroidComponentsExtension.onVariants] as at that stage, + * [AndroidXExtension.samplesProjects] would be populated. + */ +private fun Project.updateCopySampleSourceJarsTaskWithVariant(publishingVariants: List) { + val copySampleJarTask = tasks.named("copySampleSourceJars", LazyInputsCopyTask::class.java) + val configuredVariants = mutableListOf() + configurations.configureEach { config -> + if (config.name in publishingVariants) { + // Register the sample source jar as an outgoing artifact of the publishing variant + config.outgoing.artifact(copySampleJarTask.flatMap { it.destinationJar }) { + // The only place where this classifier is load-bearing is when we filter sample + // source jars out in our AndroidXDocsImplPlugin.configureUnzipJvmSourcesTasks + it.classifier = "samples-sources" + } + configuredVariants.add(config.name) + } + } + // Check that all the variants are configured because we only configure when the name matches + // and could fail silently if we never see a matching configuration + gradle.taskGraph.whenReady { + if (!configuredVariants.containsAll(publishingVariants)) { + val unconfiguredVariants = + (publishingVariants.toSet() - configuredVariants.toSet()).joinToString(", ") + throw GradleException( + "Sample source jar tasks were not configured for $unconfiguredVariants" + ) + } + } +} + +/** + * Set of targets are there to serve as stubs, but are not expected to be consumed by library + * consumers. + */ +private val setOfStubTargets = setOf("commonStubs", "jvmStubs", "linuxx64Stubs") + +internal const val PROJECT_STRUCTURE_METADATA_FILENAME = "kotlin-project-structure-metadata.json" + +private const val PROJECT_STRUCTURE_METADATA_FILEPATH = + "project_structure_metadata/$PROJECT_STRUCTURE_METADATA_FILENAME" + +internal sealed class PublishingVariant(val name: String) { + data object SourcesElements : PublishingVariant("sourcesElements") + + data object AgpKmpSourcesElements : PublishingVariant("androidSourcesElements-published") + + data object AgpLibrarySourcesElements : PublishingVariant("releaseSourcesElements") + + data object KmpSourcesElements : PublishingVariant("androidxSourcesElements") +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sources/ValidateMultiplatformSourceSetNaming.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sources/ValidateMultiplatformSourceSetNaming.kt new file mode 100644 index 0000000000000..573365518052f --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/sources/ValidateMultiplatformSourceSetNaming.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.sources + +import androidx.build.addToBuildOnServer +import androidx.build.addToCheckTask +import androidx.build.multiplatformExtension +import androidx.build.uptodatedness.cacheEvenIfNoOutputs +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget + +@DisableCachingByDefault(because = "Doesn't benefit from caching") +abstract class ValidateMultiplatformSourceSetNaming : DefaultTask() { + + @get:Input abstract val rootDir: Property + + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + fun getInputFiles(): Collection = sourceSetMap.values + + private val sourceSetMap: MutableMap = mutableMapOf() + + @set:Option( + option = "autoFix", + description = "Whether to automatically rename files instead of throwing an exception", + ) + @get:Input + var autoFix: Boolean = false + + @TaskAction + fun validate() { + // Files or entire source sets may duplicated shared across compilations, but it's more + // expensive to de-dupe them than to check the suffixes for everything multiple times. + for ((sourceFileSuffix, kotlinSourceSet) in sourceSetMap) { + for (fileOrDir in kotlinSourceSet) { + for (file in fileOrDir.walk()) { + // Kotlin source files must be uniquely-named across platforms. + if ( + file.isFile && + file.name.endsWith(".kt") && + !file.name.endsWith(".$sourceFileSuffix.kt") + ) { + val actualPath = file.toRelativeString(File(rootDir.get())) + val expectedName = "${file.name.substringBefore('.')}.$sourceFileSuffix.kt" + if (autoFix) { + val destFile = File(file.parentFile, expectedName) + file.renameTo(destFile) + logger.info("Applied fix: $actualPath -> $expectedName") + } else { + throw GradleException( + "Source files for non-common platforms must be suffixed with " + + "their target platform. Found '$actualPath' but expected " + + "'$expectedName'." + ) + } + } + } + } + } + } + + fun addTarget(project: Project, target: KotlinTarget) { + sourceSetMap[target.preferredSourceFileSuffix] = + project.files( + target.compilations + .filterNot { compilation -> + // Don't enforce suffixes for test source sets. Names can be e.g. testOnJvm + compilation.name.startsWith("test") || compilation.name.endsWith("Test") + } + .flatMap { compilation -> compilation.kotlinSourceSets } + .map { kotlinSourceSet -> kotlinSourceSet.kotlin.sourceDirectories } + .toTypedArray() + ) + } + + /** + * List of Kotlin target names which may be used as source file suffixes. Any target whose name + * does not appear in this list will use its [KotlinPlatformType] name. + */ + private val allowedTargetNameSuffixes = + setOf("android", "desktop", "jvm", "commonStubs", "jvmStubs", "linuxx64Stubs", "wasmJs") + + /** The preferred source file suffix for the target's platform type. */ + private val KotlinTarget.preferredSourceFileSuffix: String + get() = + if (allowedTargetNameSuffixes.contains(name)) { + name + } else { + platformType.name + } +} + +/** + * Ensures that multiplatform sources are suffixed with their target platform, ex. `MyClass.jvm.kt`. + * + * Must be called in afterEvaluate(). + */ +fun Project.registerValidateMultiplatformSourceSetNamingTask() { + val targets = multiplatformExtension?.targets?.filterNot { target -> target.name == "metadata" } + if (targets == null || targets.size <= 1) { + // We only care about multiplatform projects with more than one target platform. + return + } + + tasks + .register( + "validateMultiplatformSourceSetNaming", + ValidateMultiplatformSourceSetNaming::class.java, + ) { task -> + targets + .filterNot { target -> target.platformType.name == "common" } + .forEach { target -> task.addTarget(project, target) } + task.rootDir.set(rootDir.path) + task.cacheEvenIfNoOutputs() + } + .also { validateTask -> + project.addToCheckTask(validateTask) + project.addToBuildOnServer(validateTask) + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/stableaidl/StableAidlApiTasks.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/stableaidl/StableAidlApiTasks.kt new file mode 100644 index 0000000000000..48396d4177234 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/stableaidl/StableAidlApiTasks.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.stableaidl + +import androidx.build.BUILD_ON_SERVER_TASK +import androidx.build.getSupportRootFolder +import androidx.stableaidl.withStableAidlPlugin +import java.io.File +import org.gradle.api.Project + +fun Project.setupWithStableAidlPlugin() = + this.withStableAidlPlugin { ext -> + ext.checkAction.apply { + before(project.tasks.named("check")) + before(project.tasks.named(BUILD_ON_SERVER_TASK)) + before( + project.tasks.register("checkAidlApi") { task -> + task.group = "API" + task.description = + "Checks that the API surface generated Stable AIDL sources " + + "matches the checked in API surface" + } + ) + } + + ext.updateAction.apply { + before(project.tasks.named("updateApi")) + before( + project.tasks.register("updateAidlApi") { task -> + task.group = "API" + task.description = + "Updates the checked in API surface based on Stable AIDL sources" + } + ) + } + + // Don't show tasks added by the Stable AIDL plugin. + ext.taskGroup = null + + // The framework supports Stable AIDL definitions starting in SDK 36. Prior to that, we'll + // need to use manually-defined stubs. + ext.shadowFrameworkDir.set( + File(project.getSupportRootFolder(), "buildSrc/stableAidlImports") + ) + } diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt new file mode 100644 index 0000000000000..a5d53c5515315 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioPlatformUtilities.kt @@ -0,0 +1,209 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.studio + +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.util.Locale +import org.gradle.process.ExecOperations + +/** + * Utility class containing helper functions and values that change between Linux and OSX + * + * @property projectRoot the root directory of the current project + * @property studioInstallationDir the directory where studio is installed to + */ +sealed class StudioPlatformUtilities(val projectRoot: File, val studioInstallationDir: File) { + /** The file extension used for this platform's Studio archive */ + abstract val archiveExtension: String + + /** The binary directory of the Studio installation. */ + abstract val StudioTask.binaryDirectory: File + + /** A list of arguments that will be executed in a shell to launch Studio. */ + abstract val StudioTask.launchCommandArguments: List + + /** The lib directory of the Studio installation. */ + abstract val StudioTask.libDirectory: File + + /** + * The plugins directory of the Studio installation. + * + * TODO: Consider removing after Studio has switched to Kotlin 1.4 b/162414740 + */ + abstract val StudioTask.pluginsDirectory: File + + /** The license path for the Studio installation. */ + abstract val StudioTask.licensePath: String + + /** Extracts an archive at [fromPath] with [archiveExtension] to [toPath] */ + abstract fun extractArchive(fromPath: String, toPath: String, execOperations: ExecOperations) + + /** Returns the PID of the process started by this task, or `null` if not running. */ + abstract fun findProcess(): Int? + + companion object { + val osName = + if (System.getProperty("os.name").lowercase(Locale.ROOT).contains("linux")) { + "linux" + } else { + // Only works when using native version of JDK, otherwise it will fallback to x86_64 + if (System.getProperty("os.arch") == "aarch64") { + "mac_arm" + } else { + "mac" + } + } + + fun get(projectRoot: File, studioInstallationDir: File): StudioPlatformUtilities { + return if (osName == "linux") { + LinuxUtilities(projectRoot, studioInstallationDir) + } else { + MacOsUtilities(projectRoot, studioInstallationDir) + } + } + } +} + +private class MacOsUtilities(projectRoot: File, studioInstallationDir: File) : + StudioPlatformUtilities(projectRoot, studioInstallationDir) { + override val archiveExtension: String + get() = ".dmg" + + override val StudioTask.binaryDirectory: File + get() { + val file = + studioInstallationDir.walk().maxDepth(1).find { file -> + file.nameWithoutExtension.startsWith("Android Studio") && + file.extension == "app" + } + return requireNotNull(file) { "Android Studio*.app not found!" } + } + + override val StudioTask.launchCommandArguments: List + get() { + val studioBinary = File(binaryDirectory.absolutePath, "Contents/MacOS/studio") + return listOf(studioBinary.absolutePath, projectRoot.absolutePath) + } + + override val StudioTask.libDirectory: File + get() = File(binaryDirectory, "Contents/lib") + + override val StudioTask.pluginsDirectory: File + get() = File(binaryDirectory, "Contents/plugins") + + override val StudioTask.licensePath: String + get() = File(binaryDirectory, "Contents/Resources/LICENSE.txt").absolutePath + + override fun extractArchive(fromPath: String, toPath: String, execOperations: ExecOperations) { + val mountPoint = File.createTempFile("mount", null) + mountPoint.delete() + mountPoint.mkdir() + execOperations.exec { execOperation -> + with(execOperation) { + executable("hdiutil") + args("attach", fromPath, "-noverify", "-mountpoint", mountPoint.absolutePath) + } + } + execOperations.exec { execOperation -> + with(execOperation) { + commandLine("sh", "-c", "cp -R ${mountPoint.absolutePath}/*.app $toPath") + } + } + execOperations.exec { execOperation -> + with(execOperation) { + executable("hdiutil") + args("detach", mountPoint.absolutePath) + } + } + mountPoint.delete() + } + + override fun findProcess(): Int? { + println("Detecting active managed Studio instances...") + val process = + ProcessBuilder().let { + it.command(listOf("ps", "-x")) + it.redirectError(ProcessBuilder.Redirect.INHERIT) + it.start() + } + val stdout = + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + reader.lineSequence().toList() + } + process.waitFor() + val projectRootPath = projectRoot.absolutePath + return stdout + .firstOrNull { line -> line.endsWith("Contents/MacOS/studio $projectRootPath") } + ?.substringBefore(' ') + ?.toIntOrNull() + } +} + +private class LinuxUtilities(projectRoot: File, studioInstallationDir: File) : + StudioPlatformUtilities(projectRoot, studioInstallationDir) { + override val archiveExtension: String + get() = ".tar.gz" + + override val StudioTask.binaryDirectory: File + get() = File(studioInstallationDir, "android-studio") + + override val StudioTask.launchCommandArguments: List + get() { + val studioBinary = File(binaryDirectory, "bin/studio") + return listOf(studioBinary.absolutePath, projectRoot.absolutePath) + } + + override val StudioTask.pluginsDirectory: File + get() = File(binaryDirectory, "plugins") + + override val StudioTask.libDirectory: File + get() = File(binaryDirectory, "lib") + + override val StudioTask.licensePath: String + get() = File(binaryDirectory, "LICENSE.txt").absolutePath + + override fun extractArchive(fromPath: String, toPath: String, execOperations: ExecOperations) { + execOperations.exec { execOperation -> + with(execOperation) { + executable("tar") + args("-xf", fromPath, "-C", toPath) + } + } + } + + override fun findProcess(): Int? { + println("Detecting active managed Studio instances...") + val process = + ProcessBuilder().let { + it.command(listOf("ps", "-x")) + it.redirectError(ProcessBuilder.Redirect.INHERIT) + it.start() + } + val stdout = + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + reader.lineSequence().toList() + } + process.waitFor() + val projectRootPath = projectRoot.absolutePath + return stdout + .firstOrNull { line -> line.endsWith("com.intellij.idea.Main $projectRootPath") } + ?.substringBefore(' ') + ?.toIntOrNull() + } +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt new file mode 100644 index 0000000000000..f7254065b2f9c --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/studio/StudioTask.kt @@ -0,0 +1,484 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.studio + +import androidx.build.OperatingSystem +import androidx.build.ProjectLayoutType +import androidx.build.getOperatingSystem +import androidx.build.getSdkPath +import androidx.build.getSupportRootFolder +import androidx.build.getVersionByName +import com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.security.MessageDigest +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.internal.tasks.userinput.UserInputHandler +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.internal.service.ServiceRegistry +import org.gradle.process.ExecOperations +import org.gradle.work.DisableCachingByDefault + +/** + * Base task with common logic for updating and launching studio in both the frameworks/support + * project and playground projects. Project-specific configuration is provided by [RootStudioTask] + * and [PlaygroundStudioTask]. + */ +@DisableCachingByDefault(because = "the purpose of this task is to launch Studio") +abstract class StudioTask : DefaultTask() { + + @get:Input + @get:Option(option = "acceptTos", description = "Accept Android Studio Terms of Service") + @get:Optional + abstract val acceptTos: Property + + // TODO: support -y and --update-only options? Can use @Option for this + @TaskAction + fun studiow() { + validateEnvironment() + install() + installKtfmtPlugin() + writeAndroidSdkPath() + launch() + } + + private val platformUtilities by lazy { + StudioPlatformUtilities.get(projectRoot, studioInstallationDir) + } + + @get:Inject abstract val archiveOperations: ArchiveOperations + + @get:Inject abstract val execOperations: ExecOperations + + @get:Inject abstract val fileSystemOperations: FileSystemOperations + + /** + * If `true`, checks for `ANDROIDX_PROJECTS` environment variable to decide which projects need + * to be loaded. + */ + @get:Internal protected open val requiresProjectList: Boolean = true + + @get:Internal protected val projectRoot: File = project.rootDir + + @get:Internal protected open val installParentDir: File = project.rootDir + + private val studioVersion by lazy { project.getVersionByName("androidStudio") } + + /** Directory name (not path) that Studio will be unzipped into. */ + private val studioDirectoryName: String + get() { + val osName = StudioPlatformUtilities.osName + return "android-studio-$studioVersion-$osName" + } + + /** Filename (not path) of the Studio archive */ + private val studioArchiveName: String + get() = studioDirectoryName + platformUtilities.archiveExtension + + /** + * The install directory containing Studio + * + * Note: Given that the contents of this directory changes a lot, we don't want to annotate this + * property for task avoidance - it's not stable enough for us to get any value out of this. + */ + private val studioInstallationDir by lazy { + File(installParentDir, "studio/$studioDirectoryName") + } + + /** Absolute path of the Studio archive */ + private val studioArchivePath: String by lazy { + File(studioInstallationDir.parentFile, studioArchiveName).absolutePath + } + + private val studioConfigBaseDir = + File(System.getenv("HOME"), ".AndroidStudioAndroidX/config").also { it.mkdirs() } + + /** Directory where Studio downloads plugins to */ + private val studioPluginDir = File(studioConfigBaseDir, "plugins").also { it.mkdirs() } + + private val studioOptionsDir = File(studioConfigBaseDir, "options").also { it.mkdirs() } + + private val studioKtfmtPluginVersion by lazy { project.getVersionByName("ktfmtIdeaPlugin") } + + /** + * This ID changes for each ktfmt plugin version; see + * https://plugins.jetbrains.com/plugin/14912-ktfmt/versions/stable and you'll see the number in + * the redirection URL when hovering over the [studioKtfmtPluginVersion] you want downloaded + */ + private val studioKtfmtPluginId = "666004" + + private val studioKtfmtPluginDownloadUrl = + "https://downloads.marketplace.jetbrains.com/files/14912/$studioKtfmtPluginId/ktfmt_idea_plugin-$studioKtfmtPluginVersion.zip" + + /** Storage location for the ktfmt plugin zip file */ + private val studioKtfmtPluginZip = File(studioPluginDir, "ktfmt-$studioKtfmtPluginVersion.zip") + + /** Download ktfmt plugin zip file and run `shasum -a 256 ./path/to/zip` to get checksum */ + private val studioKtfmtPluginChecksum = + "869ceba41f78adc27bd6afed1bf6ba51cbd286f97ac0f6b7b5cf0058417ed242" + + /** The idea.properties file that we want to tell Studio to use */ + @get:Internal protected abstract val ideaProperties: File + + /** The studio.vmoptions file that we want to start Studio with */ + @get:Internal + open val vmOptions = File(project.getSupportRootFolder(), "development/studio/studio.vmoptions") + + /** The path to the SDK directory used by Studio. */ + @get:Internal + open val localSdkPath = project.getSdkPath().relativeTo(project.getSupportRootFolder()) + + /** List of additional environment variables to pass into the Studio application. */ + @get:Internal open val additionalEnvironmentProperties: Map = emptyMap() + + private val licenseAcceptedFile: File by lazy { + File("$studioInstallationDir/STUDIOW_LICENSE_ACCEPTED") + } + + /** Ensure that we can launch Studio without issue. */ + private fun validateEnvironment() { + if (System.getenv().containsKey("SSH_CLIENT") && !System.getenv().containsKey("DISPLAY")) { + throw GradleException( + """ + Studio must be run from a graphical session. + + Could not read DISPLAY environment variable. If you are using SSH into a remote + machine, consider using either ssh -X or switching to Chrome Remote Desktop. + """ + .trimIndent() + ) + } + } + + /** Install Studio and removes any old installation files if they exist. */ + private fun install() { + val successfulInstallFile = File("$studioInstallationDir/INSTALL_SUCCESSFUL") + if (!licenseAcceptedFile.exists() && !successfulInstallFile.exists()) { + // Attempt to remove any old installations in the parent studio/ folder + studioInstallationDir.parentFile.deleteRecursively() + // Create installation directory and any needed parent directories + studioInstallationDir.mkdirs() + downloadStudioArchive( + execOperations, + studioVersion, + studioArchiveName, + studioArchivePath, + ) + println("Extracting archive...") + extractStudioArchive() + // Finish install process + successfulInstallFile.createNewFile() + } + } + + private fun installKtfmtPlugin() { + if ( + File( + studioPluginDir, + "ktfmt_idea_plugin/lib/ktfmt_idea_plugin-$studioKtfmtPluginVersion.jar", + ) + .exists() + ) { + return + } else { + File(studioPluginDir, "ktfmt_idea_plugin").deleteRecursively() + } + + println("Downloading ktfmt plugin from $studioKtfmtPluginDownloadUrl") + execOperations.exec { execSpec -> + with(execSpec) { + executable("curl") + args(studioKtfmtPluginDownloadUrl, "--output", studioKtfmtPluginZip.absolutePath) + } + } + + studioKtfmtPluginZip.verifyChecksum() + + println("Installing ktfmt plugin into ${studioPluginDir.absolutePath}") + fileSystemOperations.copy { + it.from(archiveOperations.zipTree(studioKtfmtPluginZip)) + it.into(studioPluginDir) + } + studioKtfmtPluginZip.delete() + println("ktfmt plugin installed successfully.") + } + + /** Attempts to symlink the system-images and emulator SDK directories to a canonical SDK. */ + private fun setupSymlinksIfNeeded() { + val paths = listOf("system-images", "emulator") + if (!localSdkPath.canonicalFile.exists()) { + // We probably got the support root folder wrong. Fail gracefully. + return + } + + val relativeSdkPath = + when (val osType = getOperatingSystem()) { + OperatingSystem.MAC -> "Library/Android/sdk" + OperatingSystem.LINUX -> "Android/Sdk" + else -> { + println("Failed to locate canonical SDK, unsupported operating system: $osType") + return + } + } + + val canonicalSdkPath = File(System.getenv("HOME"), relativeSdkPath) + if (!canonicalSdkPath.exists()) { + // In the future, we might want to try a little harder to locate a canonical SDK path. + println("Failed to locate canonical SDK, not found at: $canonicalSdkPath") + return + } + + paths.forEach { path -> + val link = File(localSdkPath.canonicalFile, path) + val target = File(canonicalSdkPath, path) + if (!target.exists()) { + println("Skipping canonical SDK symlink creation, not found at: $target") + } else if (!link.exists()) { + println("Creating canonical SDK symlink for $target...") + Files.createSymbolicLink(link.toPath(), target.toPath()) + } + } + } + + /** Launches Studio if the user accepts / has accepted the license agreement. */ + private fun launch() { + if (checkLicenseAgreement(services)) { + if ( + requiresProjectList && + !System.getenv().containsKey("ANDROIDX_PROJECTS") && + !System.getenv().containsKey("PROJECT_PREFIX") + ) { + throw GradleException( + """ + Please specify which set of projects you'd like to open in studio + with ANDROIDX_PROJECTS=MAIN ./gradlew studio + or PROJECT_PREFIX=:room3: ./gradlew studio + + For possible options see settings.gradle + """ + .trimIndent() + ) + } + + // This seems like as good a time as any to set up SDK symlinks... + setupSymlinksIfNeeded() + + println("Launching studio...") + launchStudio() + } else { + println("Exiting without launching studio...") + } + } + + private fun launchStudio() { + check(ideaProperties.exists()) { + "Invalid Studio properties file location: ${ideaProperties.canonicalPath}" + } + check(vmOptions.exists()) { + "Invalid Studio vm options file location: ${vmOptions.canonicalPath}" + } + val pid = with(platformUtilities) { findProcess() } + check(pid == null) { "Found managed instance of Studio already running as PID $pid" } + val logFile = File(System.getProperty("user.home"), ".AndroidXStudioLog") + ProcessBuilder().apply { + // Can't just use inheritIO due to https://github.com/gradle/gradle/issues/16719 + // Also can't use waitFor because it causes Studio to get stuck: b/241386076 + // So, we save this output in a file and display the path to the user + redirectOutput(logFile) + redirectError(logFile) + with(platformUtilities) { command(launchCommandArguments) } + + val additionalStudioEnvironmentProperties = + mapOf( + // These environment variables are used to set up AndroidX's default + // configuration. + "STUDIO_PROPERTIES" to ideaProperties.canonicalPath, + "STUDIO_VM_OPTIONS" to vmOptions.canonicalPath, + // This environment variable prevents Studio from showing IDE inspection + // warnings + // for nullability issues, if the context is deprecated. This environment + // variable + // is consumed by InteroperabilityDetector.kt + "ANDROID_LINT_NULLNESS_IGNORE_DEPRECATED" to "true", + // This environment variable is read by AndroidXRootImplPlugin to ensure that + // Studio-initiated Gradle tasks are run against the same version of AGP that + // was + // used to start Studio, which prevents version mismatch after repo sync. + "EXPECTED_AGP_VERSION" to ANDROID_GRADLE_PLUGIN_VERSION, + ) + additionalEnvironmentProperties + platformSpecificEnvironmentProperties() + + // Append to the existing environment variables set by gradlew and the user. + environment().putAll(additionalStudioEnvironmentProperties) + start() + } + println("Studio log at $logFile") + } + + private fun platformSpecificEnvironmentProperties(): Map { + return if (System.getenv("QT_QPA_PLATFORM") == "wayland") { + // Emulators don't work on Wayland natively, make them go through XWayland + mapOf("QT_QPA_PLATFORM" to "xcb") + } else { + emptyMap() + } + } + + private fun checkLicenseAgreement(services: ServiceRegistry): Boolean { + if (!licenseAcceptedFile.exists()) { + val licensePath = with(platformUtilities) { licensePath } + + val userInput = services.get(UserInputHandler::class.java) + + if (!acceptTos.isPresent) { + val acceptAgreement = + userInput.askYesNoQuestion( + "Do you accept the license agreement at $licensePath?" + ) + if (acceptAgreement == null || !acceptAgreement) { + return false + } + } + licenseAcceptedFile.createNewFile() + } + return true + } + + private fun downloadStudioArchive( + execOperations: ExecOperations, + studioVersion: String, + filename: String, + destinationPath: String, + ) { + val url = + if (filename.contains("-mac")) { + "https://edgedl.me.gvt1.com/android/studio/install/$studioVersion/$filename" + } else { + "https://edgedl.me.gvt1.com/android/studio/ide-zips/$studioVersion/$filename" + } + val tmpDownloadPath = File("$destinationPath.tmp").absolutePath + println("Downloading $url to $tmpDownloadPath") + execOperations.exec { execSpec -> + with(execSpec) { + executable("curl") + args("-L", url, "--output", tmpDownloadPath) + } + } + + // Renames temp archive to the final archive name + Files.move(Paths.get(tmpDownloadPath), Paths.get(destinationPath)) + } + + private fun extractStudioArchive() { + val fromPath = studioArchivePath + val toPath = studioInstallationDir.absolutePath + println("Extracting to $toPath...") + platformUtilities.extractArchive(fromPath, toPath, execOperations) + // Remove studio archive once done + File(studioArchivePath).delete() + } + + private fun File.verifyChecksum() { + val actualChecksum = + MessageDigest.getInstance("SHA-256") + .also { it.update(this.readBytes()) } + .digest() + .joinToString(separator = "") { "%02x".format(it) } + + if (actualChecksum != studioKtfmtPluginChecksum) { + this.delete() + throw GradleException( + """ + Checksum mismatch for file: ${this.absolutePath} + Expected: $studioKtfmtPluginChecksum + Actual: $actualChecksum + """ + .trimIndent() + ) + } + } + + // TODO(b/443681166) Remove when fixed + private fun writeAndroidSdkPath() { + val sdkPathFile = File(studioOptionsDir, "android.sdk.path.xml") + sdkPathFile.writeText( + """ + + + + + """ + .trimIndent() + ) + } + + companion object { + private const val STUDIO_TASK = "studio" + + fun Project.registerStudioTask() { + val studioTask = + when (ProjectLayoutType.from(this)) { + ProjectLayoutType.ANDROIDX -> RootStudioTask::class.java + ProjectLayoutType.PLAYGROUND -> PlaygroundStudioTask::class.java + ProjectLayoutType.JETBRAINS_FORK -> return + } + tasks.register(STUDIO_TASK, studioTask) + } + } +} + +/** Task for launching studio in the frameworks/support project */ +@DisableCachingByDefault(because = "the purpose of this task is to launch Studio") +abstract class RootStudioTask : StudioTask() { + override val ideaProperties + get() = projectRoot.resolve("development/studio/idea.properties") +} + +/** Task for launching studio in a playground project */ +@DisableCachingByDefault(because = "the purpose of this task is to launch Studio") +abstract class PlaygroundStudioTask : RootStudioTask() { + @get:Internal + val supportRootFolder = + (project.rootProject.extensions.extraProperties).let { it.get("supportRootFolder") as File } + + /** Playground projects have only 1 setup so there is no need to specify the project list. */ + override val requiresProjectList + get() = false + + override val installParentDir + get() = supportRootFolder + + override val additionalEnvironmentProperties: Map + get() = mapOf("ALLOW_PUBLIC_REPOS" to "true") + + override val ideaProperties + get() = supportRootFolder.resolve("playground-common/idea.properties") + + override val vmOptions + get() = supportRootFolder.resolve("playground-common/studio.vmoptions") +} diff --git a/fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt new file mode 100644 index 0000000000000..3aa41988bd791 --- /dev/null +++ b/fork-project/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt @@ -0,0 +1,368 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.testConfiguration + +import com.google.gson.GsonBuilder +import groovy.xml.XmlUtil + +class ConfigBuilder { + lateinit var configName: String + var appApksModel: AppApksModel? = null + lateinit var applicationId: String + var isMicrobenchmark: Boolean = false + var isMacrobenchmark: Boolean = false + var isPostsubmit: Boolean = true + lateinit var minSdk: String + val tags = mutableListOf() + lateinit var testApkName: String + lateinit var testApkSha256: String + lateinit var testRunner: String + val additionalApkKeys = mutableListOf() + val instrumentationArgsMap = mutableMapOf() + + fun configName(configName: String) = apply { this.configName = configName } + + fun appApksModel(appApksModel: AppApksModel) = apply { this.appApksModel = appApksModel } + + fun applicationId(applicationId: String) = apply { this.applicationId = applicationId } + + fun isMicrobenchmark(isMicrobenchmark: Boolean) = apply { + this.isMicrobenchmark = isMicrobenchmark + } + + fun isMacrobenchmark(isMacrobenchmark: Boolean) = apply { + this.isMacrobenchmark = isMacrobenchmark + } + + fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit } + + fun minSdk(minSdk: String) = apply { this.minSdk = minSdk } + + fun tag(tag: String) = apply { this.tags.add(tag) } + + fun additionalApkKeys(keys: List) = apply { additionalApkKeys.addAll(keys) } + + fun testApkName(testApkName: String) = apply { this.testApkName = testApkName } + + fun testApkSha256(testApkSha256: String) = apply { this.testApkSha256 = testApkSha256 } + + fun testRunner(testRunner: String) = apply { this.testRunner = testRunner } + + fun buildJson(): String { + val gson = GsonBuilder().setPrettyPrinting().create() + val instrumentationArgsList = mutableListOf() + instrumentationArgsMap + .filter { it.key !in INST_ARG_BLOCKLIST } + .forEach { (key, value) -> instrumentationArgsList.add(InstrumentationArg(key, value)) } + instrumentationArgsList.addAll( + if (isMicrobenchmark && !isPostsubmit) { + listOf( + InstrumentationArg("notAnnotation", "androidx.test.filters.FlakyTest"), + InstrumentationArg("androidx.benchmark.dryRunMode.enable", "true"), + ) + } else { + listOf(InstrumentationArg("notAnnotation", "androidx.test.filters.FlakyTest")) + } + ) + val appApk = singleAppApk() + val values = + mapOf( + "name" to configName, + "minSdkVersion" to minSdk, + "testSuiteTags" to tags, + "testApk" to testApkName, + "testApkSha256" to testApkSha256, + "appApk" to appApk?.name, + "appApkSha256" to appApk?.sha256, + "instrumentationArgs" to instrumentationArgsList, + "additionalApkKeys" to additionalApkKeys, + ) + return gson.toJson(values) + } + + fun buildXml(): String { + val sb = StringBuilder() + sb.append(XML_HEADER_AND_LICENSE) + sb.append(CONFIGURATION_OPEN) + .append(MIN_API_LEVEL_CONTROLLER_OBJECT.replace("MIN_SDK", minSdk)) + tags.forEach { tag -> sb.append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", tag)) } + sb.append(MODULE_METADATA_TAG_OPTION.replace("APPLICATION_ID", applicationId)) + .append(WIFI_DISABLE_OPTION) + .append(FLAKY_TEST_OPTION) + if (!isPostsubmit && (isMicrobenchmark || isMacrobenchmark)) { + sb.append(BENCHMARK_PRESUBMIT_INST_ARGS) + } + val instrumentationArgsList = mutableListOf() + instrumentationArgsMap + .filter { it.key !in INST_ARG_BLOCKLIST } + .forEach { (key, value) -> instrumentationArgsList.add(InstrumentationArg(key, value)) } + if (isMicrobenchmark || isMacrobenchmark) { + instrumentationArgsList.add( + InstrumentationArg("androidx.benchmark.output.payload.testApkSha256", testApkSha256) + ) + if (isMacrobenchmark) { + instrumentationArgsList.addAll( + listOf( + InstrumentationArg( + "androidx.benchmark.output.payload.appApkSha256", + checkNotNull(appApksModel?.sha256()) { + "app apk sha should be provided for macrobenchmarks." + }, + ), + // suppress BaselineProfileRule in CI to save time + InstrumentationArg("androidx.benchmark.enabledRules", "Macrobenchmark"), + ) + ) + } + } + instrumentationArgsList.forEach { (key, value) -> + sb.append( + """ +