├── settings.gradle ├── src ├── main │ └── groovy │ │ ├── cash.bdo.scalroid.gradle_ │ │ ├── org │ │ └── example │ │ │ └── GreetingToFileTask.groovy │ │ └── cash │ │ └── bdo │ │ ├── ScalaDeDuplicateClassesTask.groovy │ │ └── ScalaAndroidCompatPlugin.groovy └── test │ └── java │ └── org │ └── example │ └── GreetingPluginTest.java ├── .gitignore ├── README.md └── LICENSE /settings.gradle: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/groovy/cash.bdo.scalroid.gradle_: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.gradle.scala' 3 | id 'org.gradle.application' 4 | } 5 | 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_1_8 8 | targetCompatibility = JavaVersion.VERSION_1_8 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /src/test/java/org/example/GreetingPluginTest.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import static org.gradle.internal.impldep.org.junit.Assert.assertTrue; 4 | 5 | import org.gradle.api.Project; 6 | import org.gradle.internal.impldep.org.junit.Test; 7 | import org.gradle.testfixtures.ProjectBuilder; 8 | 9 | public class GreetingPluginTest { 10 | @Test 11 | public void greeterPluginAddsGreetingTaskToProject() { 12 | Project project = ProjectBuilder.builder().build(); 13 | project.getPluginManager().apply("cash.bdo.scalroid"); 14 | 15 | assertTrue(project.getTasks().getByName("testToFile") instanceof GreetingToFileTask); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/groovy/org/example/GreetingToFileTask.groovy: -------------------------------------------------------------------------------- 1 | package org.example 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.file.RegularFileProperty 5 | import org.gradle.api.tasks.OutputFile 6 | import org.gradle.api.tasks.TaskAction 7 | 8 | abstract class GreetingToFileTask extends DefaultTask { 9 | @OutputFile 10 | abstract RegularFileProperty getDestination() 11 | 12 | @TaskAction 13 | def greet() { 14 | def file = destination.get().asFile 15 | file.parentFile.mkdirs() 16 | file.write 'Hello!' 17 | 18 | println("write to file `${file.name}` succeed~") 19 | 20 | // test throws: 21 | // throw new RuntimeException() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/groovy/cash/bdo/ScalaDeDuplicateClassesTask.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023-present, Chenai Nakam(chenai.nakam@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cash.bdo 18 | 19 | import org.gradle.api.DefaultTask 20 | import org.gradle.api.file.Directory 21 | import org.gradle.api.file.DirectoryProperty 22 | import org.gradle.api.file.FileType 23 | import org.gradle.api.internal.file.DefaultFileTreeElement 24 | import org.gradle.api.logging.Logger 25 | import org.gradle.api.provider.Property 26 | import org.gradle.api.provider.SetProperty 27 | import org.gradle.api.tasks.InputDirectory 28 | import org.gradle.api.tasks.Internal 29 | import org.gradle.api.tasks.OutputDirectory 30 | import org.gradle.api.tasks.TaskAction 31 | import org.gradle.internal.nativeintegration.filesystem.FileSystem 32 | import org.gradle.work.ChangeType 33 | import org.gradle.work.Incremental 34 | import org.gradle.work.InputChanges 35 | 36 | import javax.inject.Inject 37 | 38 | /** 39 | * @author Chenai Nakam(chenai.nakam@gmail.com) 40 | * @version 1.0 24/02/2023 41 | */ 42 | abstract class ScalaDeDuplicateClassesTask extends DefaultTask { 43 | @Inject 44 | abstract FileSystem getFileSystem() 45 | 46 | @Internal 47 | abstract Property getNAME_PLUGIN() 48 | 49 | @Internal 50 | abstract Property getLOG() 51 | //@Internal 52 | //abstract Property getInputDirPathLength() 53 | 54 | @Internal 55 | abstract SetProperty getPackageOrNamesEvicts() 56 | 57 | @Internal 58 | abstract SetProperty getPackageOrNamesExcludes() 59 | 60 | @Incremental 61 | @InputDirectory 62 | abstract DirectoryProperty getInputDir() 63 | 64 | @OutputDirectory 65 | abstract DirectoryProperty getOutputDir() 66 | 67 | @TaskAction 68 | def execute(InputChanges inputChanges) { 69 | // TODO: 实测,输入输出目录不应相同,会导致该任务始终处于`UP-TO-DATE`状态。 70 | final boolean isInOutSame = isInOutTheSameDir(inputDir.get(), outputDir.get()) 71 | if (isInOutSame) LOG.get().warn "${NAME_PLUGIN.get()} ---> [deduplicate.execute] The `inputDir` was detected to be the same as `outputDir`, the result may be incorrect!" 72 | 73 | inputChanges.getFileChanges(inputDir).each { change -> 74 | switch (change.fileType) { 75 | case FileType.MISSING: return 76 | case FileType.DIRECTORY: 77 | // 下面的`DefaultFileTreeElement.copyTo()`有`mkdirs()`操作 78 | //outputDir.file(change.normalizedPath).get().asFile.mkdirs() 79 | return 80 | } 81 | 82 | final inputDirPath = inputDir.get().asFile.path 83 | final inputDirPathLength = inputDirPath.length() + (inputDirPath.endsWith('/') ? 0 : 1) 84 | 85 | final pathRelative = change.normalizedPath.substring(inputDirPathLength) 86 | final targetFile = isInOutSame 87 | // 但这里即使`inputDir`与`outputDir`相同,也要创建新的文件。否则删不掉。 88 | ? outputDir.file(change.normalizedPath).get().asFile 89 | // 只有这样才能创建真正位于`outputDir`下的文件。否则如果像上一行那样,即使`inputDir`与`outputDir`不同,targetFile 也与`change.file`相同。 90 | : outputDir.file(pathRelative).get().asFile 91 | 92 | //noinspection GroovyFallthrough 93 | switch (change.changeType) { 94 | case ChangeType.REMOVED: 95 | targetFile.delete() 96 | break 97 | case ChangeType.ADDED: 98 | case ChangeType.MODIFIED: 99 | if (isFileHitEvict(change.file, inputDirPathLength)) targetFile.delete() //. 100 | else if (!isInOutSame) { 101 | LOG.get().info "${NAME_PLUGIN.get()} ---> [deduplicate.execute]copy to:$targetFile" 102 | DefaultFileTreeElement.of(change.file, fileSystem).copyTo(targetFile) 103 | } 104 | break 105 | } 106 | } 107 | } 108 | 109 | boolean isInOutTheSameDir(Directory input, Directory output) { 110 | return input.asFile.path == output.asFile.path 111 | } 112 | 113 | boolean isFileHitEvict(File file, int inputDirPathLength) { 114 | final int destLen = inputDirPathLength 115 | final Set excludes = packageOrNamesEvicts.get() + packageOrNamesExcludes.get() 116 | if (excludes.isEmpty()) { 117 | // androidTest, unitTest 为空较为正常。 118 | // 由于这个插件是我写的,如果有错,也是我的错(不是用户的错),所以…不能抛异常,最多给个警告表示有这回事即可。 119 | LOG.get().info "${NAME_PLUGIN.get()} ---> [deduplicate.isFileHitEvict] Are the parameters set correctly? `packageOrNamesEvicts` and `packageOrNamesExcludes` are both empty, and the output may cause duplication error!" 120 | //return false // 下面的`excludes.any{}`也会立即返回 false。 121 | } 122 | final hit = excludes.any { pkgName -> 123 | //file.path.substring(destLen) == (pkgName + '.class') // 可能后面跟的不是`.class`而是`$1.class`、`$xxx.class`。 124 | //file.path.substring(destLen).startsWith(pkgName) // 改写如下: 125 | file.path.indexOf(pkgName, destLen) == destLen && file.path.indexOf('/', destLen + pkgName.length()) < 0 126 | //hobby/wei/c/L$3.class 127 | //hobby/wei/c/LOG.class 128 | && (file.path.indexOf(pkgName + '.', destLen) == destLen || file.path.indexOf(pkgName + '$', destLen) == destLen) 129 | } 130 | LOG.get().info "${NAME_PLUGIN.get()} ---> [deduplicate.isFileHitEvict] ${hit ? '^^^' : 'NOT'} HIT:$file" 131 | return hit 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scalroid 2 | 3 | [![Join the chat at https://gitter.im/scalroid/community](https://badges.gitter.im/scalroid/community.svg)](https://gitter.im/scalroid/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | [**`Discord#scala-android`**](https://discord.gg/RrtXCpsUZ5) 6 | 7 | A `scala-kotlin-java` joint compilation plugin built on `Gradle`, for `native Android`. 8 | 9 | The plugin was built with `ScalaBasePlugin`, which is also an official plugin of `Gradle`, cooperates perfectly with the `Android` official plugin, is a supplement. It 10 | takes very little code to put the two together but functional maturation, includes `androidTest` and `unitTest`. There are no conflicts or incompatibilities, even if it 11 | does, it's easy to update and maintain. 12 | 13 | Now, this plugin is well developed and ready for official use. 14 | 15 | * Also refers demo project [scalroid-build-demo](https://github.com/chenakam/scalroid-build-demo) 16 | 17 | ## Supported versions 18 | 19 | | Gradle | Android Plugin | Kotlin Plugin | Scala (this plugin compiles) | 20 | |-----------------|-------------------|---------------------|------------------------------| 21 | | `7.6.2` ~ `8.4` | `7.4.0` ~ `8.1.3` | `1.7.20` ~ `1.9.20` | `2.10.x` ~ `3.x` | 22 | 23 | * The Scala version fully supports the `ScalaPlugin` of gradle, see official documentation: 24 | https://docs.gradle.org/current/userguide/scala_plugin.html#sec:configure_zinc_compiler 25 | For details about how to set `zincVersion`, see the example code below. 26 | 27 | * Known issues: 28 | - Since the Android's built-in _`JDK/JRE`_ does NOT have implements the class `java.lang.ClassValue`, but some classes require it, such as `scala.reflect.ClassTag`. 29 | So i have made a copy [_**here**_](https://github.com/bdo-cash/assoid/blob/v.gradle/src/main/scala/java/lang/ClassValue.java). 30 | Or as an alternative, you can set _`cacheDisabled = true`_ 31 | in [**`ClassTag`**](https://github.com/scala/scala/blob/2.12.x/src/library/scala/reflect/ClassTag.scala#L140) to avoid method calls to **`ClassValue`**. To achieve 32 | this, you can use [_**`classTagDisableCache`**_](https://github.com/bdo-cash/assoid/blob/v.gradle/src/main/scala/scala/compat/classTagDisableCache.scala) (it works 33 | well even after `Proguard/R8`) at the very beginning of your app startup ( 34 | e.g. [_**`AbsApp`**_](https://github.com/bdo-cash/assoid/blob/v.gradle/src/main/scala/hobby/wei/c/core/AbsApp.scala#L53)). But you still need to define a simple 35 | class so that it can be found at runtime: 36 | ```java 37 | package java.lang; 38 | //import hobby.wei.c.anno.proguard.Keep$; 39 | //@Keep$ 40 | public abstract class ClassValue { 41 | protected abstract T computeValue(Class type); 42 | public T get(Class type) { return null; } 43 | public void remove(Class type) {} 44 | } 45 | ``` 46 | - Since the Gradle have bugs in `Scala incremental compilation` _~~and did not fixed in version **v7.x**: [#23202](https://github.com/gradle/gradle/issues/23202)~~_ 47 | (but has been fixed in and after **v7.6.2**). 48 | Nevertheless, Gradle **v8.0.1 ~ v8.4** is recommended, and scala incremental compilation works well. 49 | 50 | ## Usage 51 | 52 | 1. Clone this repository to your project's `buildSrc` directory (**optional**): 53 | 54 | ```bash 55 | cd 56 | git clone git@github.com:chenakam/scalroid.git buildSrc 57 | ``` 58 | 59 | * Below is your project's directory structure as possibly looks like: 60 | 61 | ```text 62 | /your_project 63 | /app 64 | /src 65 | /main 66 | /java # Your java/kotlin code dir 67 | /scala # Your scala code dir (java files can also compile) 68 | /build.gradle 69 | /... 70 | /buildSrc # This repo dir 71 | /other_modules 72 | /... 73 | /build.gradle 74 | /... 75 | ``` 76 | 77 | 2. Add `id 'cash.bdo.scalroid' xxx` to your `/build.gradle` file: 78 | 79 | * See also https://plugins.gradle.org/plugin/cash.bdo.scalroid 80 | 81 | ```groovy 82 | // ... 83 | plugins { 84 | id 'com.android.application' version '8.1.3' apply false 85 | id 'com.android.library' version '8.1.3' apply false 86 | id 'org.jetbrains.kotlin.android' version '1.9.20' apply false 87 | 88 | // TODO: if you have not clone the dir `buildSrc/`, you need to uncomment the `version` filed. 89 | id 'cash.bdo.scalroid' /*version '[1.6-gradle8,)'*/ apply false 90 | } 91 | ``` 92 | 93 | 3. Add `apply plugin: 'cash.bdo.scalroid'` to your `app/build.gradle` file: 94 | 95 | ```groovy 96 | plugins { 97 | id 'com.android.application' 98 | // Alternatively, for your library subproject 99 | //id 'com.android.library' 100 | 101 | id 'org.jetbrains.kotlin.android' // Required 102 | // or (abbr) 103 | //id 'kotlin-android' 104 | 105 | // This plugin 106 | id 'cash.bdo.scalroid' 107 | } 108 | 109 | android { 110 | scalroid { 111 | scala.zincVersion = '1.8.0' 112 | // scalaCodeReferToKt = false // Take looks below 113 | // ktCodeReferToScala = true 114 | // Whether to expand `R.jar`, so as to fix the problem of `R.id.xxx` marked red in Scala code. 115 | // This will bring the `R.jar` under `External Libraries`. 116 | // setAppRJarAsLib = true 117 | // javaDirsExcludes = ['src/main/kotlin', 'src/aaa/xxx'] 118 | } 119 | // ... 120 | } 121 | 122 | dependencies { 123 | implementation "androidx.core:core-ktx:1.12.0" 124 | implementation 'androidx.appcompat:appcompat:1.6.1' 125 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 126 | // ... 127 | 128 | // `scala-library` must be set. Scala 2 or Scala 3 129 | 130 | // Scala 2 131 | implementation "org.scala-lang:scala-library:2.12.18" 132 | // Scala 3 133 | // implementation 'org.scala-lang:scala3-library_3:3.2.0-RC2' 134 | } 135 | ``` 136 | 137 | 4. You can edit any code in `your_project/buildSrc/src` as needed, and then click the **button** with tooltip `Sync Project with Gradle Files` in the toolbar of 138 | your `Android Studio`, your modified code will be applied immediately. 139 | 140 | ## Preferences 141 | 142 | It's not that easy to get `scala` and `kotlin` code to compile together in one go. Especially when it comes to cross-referencing code. So, here are two options to help 143 | you compile through. 144 | 145 | #### _a. Leverage Java, is preferred_ 146 | 147 | You can use Java as a transition, scala and kotlin both refer to java instead of directly referencing each other. At most, you can also use kotlin code to refer directly 148 | to scala (by default). That should do it. 149 | 150 | Don't forget the following **settings** if you put java source files in a non-default folder, e.g.`src/aaa/xxx`. The default folders are `src/main/java` 151 | and `src/main/kotlin` and do not need to be set. 152 | 153 | ```groovy 154 | scalroid { 155 | javaDirsExcludes = ['src/main/xxx', 'src/aaa/xxx'] 156 | } 157 | ``` 158 | 159 | Note: `.java` (and `.scala`) files written in `src/main/scala` directory will be compiled and output by the scala compiler. `.java` files written in other directories 160 | will **NOT** be compiled and output by scala even if they are referenced by `.scala` files (which would be compiled for output by the java compiler). But if NOT 161 | configured in `javaDirsExcludes`, output is compiled by scala by default, which may results in duplicate `.class` files and an error is reported. If similar error occurs, 162 | try adding the directories to `javaDirsExcludes`. 163 | 164 | In other words, if you want scala to compile, you should written `.java` (and `.scala`) files in the `src/main/scala` directory (or other directories you configured 165 | in `sourceSets`), otherwise you should written them in the `java/kotlin` directories. 166 | 167 | #### _b. Cross-referencing is inevitable_ 168 | 169 | There are two parameters you can use conveniently, and they are: 170 | 171 | ```groovy 172 | scalroid { 173 | scalaCodeReferToKt = false // `false` by default 174 | ktCodeReferToScala = true // `true` by default 175 | } 176 | ``` 177 | 178 | The default value means **the kotlin code references scala code, but it cannot be back referenced** because of *scala-kotlin cross compilation* has not implemented. If 179 | this is the case, do nothing else, leave the defaults, and the project compiles. Or the opposite one-way reference works: 180 | 181 | ```groovy 182 | scalroid { 183 | // One-way reference works 184 | scalaCodeReferToKt = false // `false` by default 185 | ktCodeReferToScala = true // `true` by default 186 | 187 | // Alternatively: 188 | // The opposite one-way reference also works too 189 | scalaCodeReferToKt = true 190 | ktCodeReferToScala = false 191 | } 192 | ``` 193 | 194 | But given the existence of such a situation of **scala/kotlin code tends to cross-reference**, I also made compatible reluctantly so that we can compile finally. It's 195 | theoretically set up like this (but the truth is not so simple): 196 | 197 | ```groovy 198 | scalroid { 199 | scalaCodeReferToKt = true 200 | ktCodeReferToScala = true 201 | } 202 | ``` 203 | 204 | * If this is set, once you `clean` the project, you will got errors the next time you compile. 205 | 206 | The correct steps are (tentatively and reluctantly): 207 | 208 | 1. `Clean` project (run `./gradlew clean`); 209 | 2. Comment out any code in `scala` that references `kotlin` (or vice versa) and uniformize the two parameters setting to maintain a one-way reference (no need to 210 | click `Sync`, which doesn't matter); 211 | 3. Try compiling until you pass (`./gradlew :app:compile{VARIANT}JavaWithJavac`), `kotlin-classes` and `scala-classes` in `app/build/tmp/` have output associated 212 | with `VARIANT`; 213 | 4. Sets the two parameters both to `true` and **uncomment** any code commented out in Step 2; 214 | 5. Try compiling until you pass. 215 | 216 | * Note: repeat these steps each time you `clean` your project. 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/groovy/cash/bdo/ScalaAndroidCompatPlugin.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022-present, Chenai Nakam(chenai.nakam@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cash.bdo 18 | 19 | import org.example.GreetingToFileTask 20 | import org.gradle.api.Plugin 21 | import org.gradle.api.Project 22 | import org.gradle.api.ProjectConfigurationException 23 | import org.gradle.api.Task 24 | import org.gradle.api.artifacts.Configuration 25 | import org.gradle.api.artifacts.Dependency 26 | import org.gradle.api.artifacts.type.ArtifactTypeDefinition 27 | import org.gradle.api.file.FileCollection 28 | import org.gradle.api.file.SourceDirectorySet 29 | import org.gradle.api.internal.tasks.DefaultScalaSourceDirectorySet 30 | import org.gradle.api.internal.tasks.DefaultScalaSourceSet 31 | import org.gradle.api.internal.tasks.TaskDependencyFactory 32 | import org.gradle.api.logging.Logger 33 | import org.gradle.api.logging.Logging 34 | import org.gradle.api.model.ObjectFactory 35 | import org.gradle.api.plugins.JavaBasePlugin 36 | import org.gradle.api.plugins.scala.ScalaBasePlugin 37 | import org.gradle.api.plugins.scala.ScalaPlugin 38 | import org.gradle.api.plugins.scala.ScalaPluginExtension 39 | import org.gradle.api.provider.ListProperty 40 | import org.gradle.api.provider.Property 41 | import org.gradle.api.tasks.ScalaSourceDirectorySet 42 | import org.gradle.api.tasks.TaskProvider 43 | import org.gradle.api.tasks.compile.AbstractCompile 44 | import org.gradle.api.tasks.compile.JavaCompile 45 | import org.gradle.api.tasks.scala.IncrementalCompileOptions 46 | import org.gradle.api.tasks.scala.ScalaCompile 47 | import org.gradle.api.tasks.scala.ScalaDoc 48 | 49 | import javax.annotation.Nullable 50 | import javax.inject.Inject 51 | import java.util.concurrent.Callable 52 | 53 | import static org.gradle.api.internal.lambdas.SerializableLambdas.spec 54 | 55 | interface ScalroidExtension { 56 | static final DEF_MESSAGE = 'Hi, SCALA lover!' 57 | static final DEF_GREETER = "${ScalaAndroidCompatPlugin.ID_PLUGIN.toUpperCase()} Developer~" 58 | 59 | /** 默认值 false,表示:kotlin 代码引用 scala,但 scala 不引用 kotlin。这样的话`Task`不涉及循环依赖,编译正常进行。 60 | * 但更为复杂的情况是: 61 | * 代码交叉引用:即 kotlin 引用 scala,scala 又引用 kt。这里提供一个苟且的技巧: 62 | * 1. Clean 项目(运行`./gradlew clean`); 63 | * 2. 先把 scala 中引用 kt 的代码都注释掉,并将本变量置为默认值 false(不用点击 Sync Now,点击也无妨); 64 | * 3. 编译直到通过(`./gradlew :app:compile{VARIANT}JavaWithJavac`),目录 app/build/tmp/ 下的 kotlin-classes 和 scala-classes 都有 VARIANT 相关的输出; 65 | * 4. 将本变量置为默认值 true,并将在步骤 2 中注释掉的 scala 中引用 kt 的代码都取消注释; 66 | * 5. 编译直到成功。 67 | * 68 | * 注意:每次 Clean 项目,都要将上述步骤重来一遍。*/ 69 | Property getScalaCodeReferToKt() 70 | /** 默认值 true。其它同上。看 scala/kotlin 哪个注释的代价小。*/ 71 | Property getKtCodeReferToScala() 72 | /** 是否将`R.jar`展开(Expand),这样会将其纳入到`External Libraries`下管理。*/ 73 | Property getSetAppRJarAsLib() 74 | 75 | ListProperty getJavaDirsExcludes() 76 | 77 | Property getMessage() 78 | 79 | Property getGreeter() 80 | } 81 | 82 | /** 83 | * @author Chenai Nakam(chenai.nakam@gmail.com) 84 | * @version 1.0 xx/07/2022 85 | */ 86 | class ScalaAndroidCompatPlugin implements Plugin { 87 | // TODO: e.g. 88 | // `./gradlew :app:compileGithubDebugJavaWithJavac --stacktrace` 89 | // `./gradlew :app:compileGithubDebugJavaWithJavac --info` for LOG.info('...') 90 | // `./gradlew :app:compileGithubDebugJavaWithJavac --debug` 91 | // LOG 避免与某些 Closure 的父类 LOGGER 相冲突 92 | protected static final Logger LOG = Logging.getLogger(ScalaAndroidCompatPlugin) 93 | 94 | static final NAME_PLUGIN = 'scalroid' 95 | static final NAME_ANDROID_EXTENSION = 'android' 96 | static final NAME_SCALA_EXTENSION = 'scala' 97 | static final NAME_KOTLIN_EXTENSION = 'kotlin' 98 | static final ID_PLUGIN = "cash.bdo.$NAME_PLUGIN" 99 | static final ID_ANDROID_APP = 'com.android.application' 100 | static final ID_ANDROID_LIB = 'com.android.library' 101 | // 这个用法见`com.android.build.gradle.api.AndroidBasePlugin`文档。 102 | static final ID_ANDROID_BASE = 'com.android.base' 103 | static final ID_KOTLIN_ANDROID = 'org.jetbrains.kotlin.android' 104 | static final ID_KOTLIN_ANDROID_ABBR = 'kotlin-android' // 上面的缩写 105 | static final ID_PRE_BUILD = 'preBuild' 106 | static final ID_TEST_TO_FILE = 'testToFile' 107 | 108 | static final main = 'main' 109 | static final test = 'test' 110 | static final androidTest = 'androidTest' 111 | static final unitTest = 'unitTest' 112 | static final debug = 'debug' 113 | static final release = 'release' 114 | 115 | private boolean isCheckArgsWarningShowed = false 116 | 117 | private void checkArgsProbablyWarning(ScalroidExtension ext) { 118 | if (isCheckArgsWarningShowed) return 119 | final scalaReferKt = ext.scalaCodeReferToKt.get() 120 | final ktReferScala = ext.ktCodeReferToScala.get() 121 | final checked = false 122 | final lineMsg = "maybe error" 123 | if (scalaReferKt == ktReferScala && scalaReferKt == checked) { 124 | LOG.warn "" 125 | LOG.warn "* WARNING: parameter values may be set incorrectly (both `$checked`). Are you sure them?" 126 | LOG.warn " android {" 127 | LOG.warn " ${NAME_PLUGIN} {" 128 | LOG.warn " scalaCodeReferToKt = ${scalaReferKt} // $lineMsg" 129 | LOG.warn " ktCodeReferToScala = ${ktReferScala} // $lineMsg" 130 | LOG.warn " }" 131 | LOG.warn " ..." 132 | isCheckArgsWarningShowed = true 133 | } 134 | } 135 | 136 | private void testPrintParameters(Project project) { 137 | LOG.warn "applying plugin `$ID_PLUGIN` for ${project}" 138 | // project.path: :app, project.name: app, project.group: DemoMaterial3, project.version: unspecified 139 | //LOG.info "$NAME_PLUGIN ---> project.path: ${project.path}, project.name: ${project.name}, project.group: ${project.group}, project.version: ${project.version}" 140 | //LOG.info '' 141 | } 142 | private final ObjectFactory factory 143 | private final TaskDependencyFactory dependencyFactory 144 | //private final SoftwareComponentFactory softCptFactory 145 | //private final JvmPluginServices jvmServices 146 | //private final Factory patternSetFactory 147 | 148 | @Inject 149 | ScalaAndroidCompatPlugin(ObjectFactory objectFactory, TaskDependencyFactory dependencyFactory) { 150 | this.factory = objectFactory 151 | this.dependencyFactory = dependencyFactory 152 | //this.softCptFactory = softCptFactory 153 | //this.jvmServices = jvmServices 154 | //this.patternSetFactory = patternSetFactory 155 | } 156 | 157 | @Override 158 | void apply(Project project) { 159 | testPrintParameters(project) 160 | if (![ID_ANDROID_APP, ID_ANDROID_LIB, ID_ANDROID_BASE].any { project.plugins.findPlugin(it) }) { 161 | // apply plugins 具有顺序性。 162 | throw new ProjectConfigurationException("Please apply `$ID_ANDROID_APP` or `$ID_ANDROID_LIB` plugin before applying `$ID_PLUGIN` plugin.", new Throwable()) 163 | } 164 | if (!project.plugins.findPlugin(ID_KOTLIN_ANDROID)) { 165 | throw new ProjectConfigurationException("Please apply `$ID_KOTLIN_ANDROID` (or `$ID_KOTLIN_ANDROID_ABBR` for addr) plugin before applying `$ID_PLUGIN` plugin.", new Throwable()) 166 | } 167 | ScalroidExtension extension = project.extensions.create(NAME_PLUGIN, ScalroidExtension) 168 | 169 | // 1. 应用`ScalaBasePlugin`,与标准 Android 系列插件之间没有冲突。 170 | project.pluginManager.apply(ScalaBasePlugin) // or apply 'org.gradle.scala-base' 171 | ScalaPluginExtension scalaExtension = project.extensions.getByName(NAME_SCALA_EXTENSION) 172 | addScalaPluginExtensionToScalroidClosure(extension, scalaExtension) 173 | 174 | final isLibrary = project.plugins.hasPlugin(ID_ANDROID_LIB) 175 | // 2. 设置 Scala 源代码目录,并链接编译 Task 的依赖关系。 176 | linkScalaAndroidResourcesAndTasks(project, extension, isLibrary) 177 | // 3. 最后,加入本插件的`Task`。 178 | addThisPluginTask(project, extension, scalaExtension, isLibrary) 179 | } 180 | 181 | private void addThisPluginTask(Project project, ScalroidExtension extension, ScalaPluginExtension scalaExtension, boolean isLibrary) { 182 | project.task(NAME_PLUGIN) { 183 | // 设置会优先返回(写在`build.gradle`里的) 184 | extension.scalaCodeReferToKt = false 185 | extension.ktCodeReferToScala = true 186 | extension.setAppRJarAsLib = true 187 | extension.javaDirsExcludes = factory.listProperty(String) 188 | extension.message = ScalroidExtension.DEF_MESSAGE 189 | extension.greeter = ScalroidExtension.DEF_GREETER 190 | doLast { 191 | final version = scalaExtension.zincVersion.get() 192 | // `extension.convention.plugins.get(NAME_SCALA_EXTENSION).zincVersion.get()` 193 | assert version == extension.scala.zincVersion.get() 194 | LOG.error "${extension.message.get()} by ${extension.greeter.get()}" 195 | LOG.error "Scala zinc version: $version" 196 | } 197 | } 198 | if (isLibrary) return 199 | project.tasks.register(ID_TEST_TO_FILE, GreetingToFileTask) { 200 | destination = project.objects.fileProperty() 201 | destination.set(project.layout.buildDirectory.file("${NAME_PLUGIN}/test-to-file.txt")) 202 | } 203 | project.tasks.getByName(ID_PRE_BUILD) { 204 | dependsOn NAME_PLUGIN 205 | //dependsOn ID_TEST_TO_FILE 206 | } 207 | } 208 | 209 | private void linkScalaAndroidResourcesAndTasks(Project project, ScalroidExtension scalroid, boolean isLibrary) { 210 | final androidExtension = project.extensions.getByName(NAME_ANDROID_EXTENSION) 211 | Plugin androidPlugin = isLibrary ? project.plugins.findPlugin(ID_ANDROID_LIB) : project.plugins.findPlugin(ID_ANDROID_APP) 212 | addPluginExtensionToAndroidClosure(androidExtension, scalroid) 213 | 214 | //final scalaBasePlugin = project.plugins.findPlugin(ScalaBasePlugin) 215 | final workDir = project.layout.buildDirectory.file(NAME_PLUGIN).get().asFile 216 | //project.tasks.getByName(ID_PRE_BUILD).doLast { workDir.mkdirs() } 217 | 218 | // 实测在`project.afterEvaluate`前后,`sourceSet`数量不一样。 219 | // 但是如果不执行这次,`build.gradle`中的`sourceSets.main.scala.xxx`会报错。 220 | // 在这里执行之后,会 apply `build.gradle`中的`sourceSets.main.scala.xxx`的设置。 221 | //androidExtension.sourceSets.each { sourceSet -> } 222 | // 新的问题是:`sourceSet.java.srcDirs += sourceSet.scala.srcDirs`在`project.afterEvaluate{}`里会报错(查了源码没写错): 223 | // Caused by: org.gradle.internal.typeconversion.UnsupportedNotationException: Cannot convert the provided notation to a File or URI: [/Users/weichou/git/bdo.cash/demo-material-3/app/src/githubDebug/scala]. 224 | // The following types/formats are supported: 225 | // - A String or CharSequence path, for example 'src/main/java' or '/usr/include'. - A String or CharSequence URI, for example 'file:/usr/include'. - A File instance. - A Path instance. - A Directory instance. - A RegularFile instance. - A URI or URL instance. - A TextResource instance. 226 | // 所以改为如下写法: 227 | final sourceSetConfig = { sourceSet -> // androidTest, test, main 228 | LOG.info "$NAME_PLUGIN ---> sourceSetConfig:${sourceSet.name}" 229 | 230 | //final isTest = maybeIsTest(adjustSourceSetNameToMatchVariant(sourceSet.name, null)) 231 | if (!resolveScalaSrcDirsToAndroidSourceSetsClosure(project, sourceSet, false)) { 232 | return // 已配置(至少`main`会重复配置) 233 | } 234 | // 根据`DefaultScalaSourceSet`中的这一句`scalaSourceDirectorySet.getFilter().include("**/*.java", "**/*.scala")`,表明是可以在 scala 目录下写 java 文件的。 235 | // 实测:虽然可以在 scala 目录写 java(并编译),但源码不能识别。但有该语句就能识别了(不用担心会串源码,有默认的过滤器)。 236 | sourceSet.java.srcDirs += sourceSet.scala.srcDirs // com.google.common.collect.SingletonImmutableSet 或 RegularImmutableSet 237 | // java、scala 两个编译器都会编译输出,`./gradlew :app:assemble`时会报错: 238 | // >D8: Type `xxx.TestJavaXxx` is defined multiple times: 239 | //sourceSet.java.filter.excludes += "**/scala/**" 240 | sourceSet.java.filter.exclude { //org.gradle.api.internal.file.AttributeBasedFileVisitDetails it -> 241 | final path = it.file.path 242 | final sub = path.substring(0, path.length() - it.path.length()) 243 | return sub.endsWith('/scala/') 244 | } 245 | if (sourceSet.java.srcDirs) LOG.info "$NAME_PLUGIN ---> ${sourceSet.java.srcDirs} / ${sourceSet.java.srcDirs.class}" 246 | if (sourceSet.kotlin) { 247 | final ktSrc = sourceSet.kotlin.srcDirs // com.google.common.collect.RegularImmutableSet 248 | // 同理:有了这句,kt 引用 scala 就不标红了。 249 | sourceSet.kotlin.srcDirs += sourceSet.scala.srcDirs // LinkedHashSet 250 | // 既然…那就…(保险起见,让 scala 引用 java 目录下的文件不标红)。 251 | sourceSet.scala.srcDirs += ktSrc 252 | // TODO: 改为编译输出时过滤(见`evictCompileOutputForSrcTask()`),这样可以初步进行交叉编译(需要写 java 文件做中转)。但有个问题:如果 java 文件过多,会生成并删除许多问题。 253 | // 上面`sourceSet.java.filter.exclude`不用改(直接从源文件过滤),因为它最后编译,即使有依赖`/scala/`下的文件,这些先编译完的已经纳入到 JavaCompile 的 classpath 了。 254 | // 上面没有`sourceSet.kotlin.filter.exclude`说明 KotlinCompile 不会编译 java 文件。 255 | /*sourceSet.scala.filter.exclude { // 由于`ktSrc`包含 java 目录。实测:如果没有这句,会把 java 目录下的编译到 scala-classes 下,最终导致打包 jar 时某些 Xxx.class 重复。 256 | final path = it.file.path 257 | final sub = path.substring(0, path.length() - it.path.length()) 258 | return sub.endsWith('/java/') 259 | }*/ 260 | } 261 | LOG.info "$NAME_PLUGIN ---> java.srcDirs:" + sourceSet.java.srcDirs 262 | LOG.info "$NAME_PLUGIN ---> scala.srcDirs:" + sourceSet.scala.srcDirs 263 | LOG.info "$NAME_PLUGIN ---> kotlin.srcDirs:" + (sourceSet.kotlin ? sourceSet.kotlin.srcDirs : null) 264 | LOG.info '' 265 | } 266 | androidExtension.sourceSets.whenObjectAdded { sourceSet -> // androidTest, main, test 267 | sourceSetConfig.call(sourceSet) 268 | } 269 | // 实测在 library 里,whenObjectAdded 不执行。 270 | androidExtension.sourceSets.each { sourceSet -> // ... 271 | sourceSetConfig.call(sourceSet) 272 | } 273 | final mainSourceSet = androidExtension.sourceSets.getByName(main) 274 | LOG.info '' 275 | LOG.info "$NAME_PLUGIN ---> mainSourceSet: $mainSourceSet" 276 | LOG.info '' 277 | LOG.info "|||||||||| |||||||||| |||||||||| |||||||||| |||||||||| AFTER EVALUATE |||||||||| |||||||||| |||||||||| |||||||||| ||||||||||" 278 | 279 | // 要保证`main`在`project.afterEvaluate{}`配置完成。否则实测会报错。 280 | sourceSetConfig.call(mainSourceSet) 281 | 282 | // TODO: 为了在`build.gradle`里这样写(它不能写在`project.afterEvaluate{}`里,会报错)。 283 | // dependencies { 284 | // appRJarXxx fileTree(include: ['**/R.jar'], dir: 'build/intermediates/compile_and_runtime_not_namespaced_r_class_jar') 285 | // } 286 | // 但实测无用。只有`compileOnly`等一些预置的(configurations)才能被放进`External Libraries`下管理。 287 | /*project.configurations.create('appRJarXxx', { Configuration conf -> 288 | conf.canBeResolved = true 289 | conf.canBeConsumed = false 290 | conf.visible = false 291 | conf.description = 'Fix issue of `R.id.xxx` marked red in `Scala` code.' 292 | })*/ 293 | project.afterEvaluate { 294 | LOG.info '' 295 | ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// 296 | //printConfiguration(project, 'implementation') 297 | 298 | //dependencies { 299 | // 这里的`implementation`就是`Configuration`的名字,是在`Android`插件中定义的。 300 | // 所以需要什么就在`dependencies`中找什么,然后按照下面的代码示例查找。 301 | // implementation "androidx.core:core-ktx:$ktx.Version" 302 | // implementation "org.scala-lang:scala-library:$scala2.Version" 303 | //} 304 | // 为了让`ScalaCompile`能够找到`Scala`的版本,需要在`ScalaCompile`的`classpath`中添加包含"library"的 jar。详见:`scalaRuntime.inferScalaClasspath(compile.getClasspath())`。 305 | // `classpath`需要`FileCollection`,而`Configuration`继承自`FileCollection`,所以可以直接使用`Configuration`。 306 | // 具体写在下边`x.register("xxx", ScalaCompile)`。 307 | 308 | ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// 309 | final variantsAll = androidExtension.testVariants + androidExtension.unitTestVariants + (isLibrary ? androidExtension.libraryVariants : androidExtension.applicationVariants) 310 | final variantsNames = new HashSet() 311 | final variantsMap = new HashMap() 312 | final variantSourceSetMap = new HashMap() 313 | variantsAll.each { variant -> 314 | variantsNames.add(variant.name) 315 | variantsMap.put(variant.name, variant) 316 | } 317 | 318 | LOG.info '' 319 | LOG.info "$NAME_PLUGIN ---> variantsNames: ${variantsNames.join(", ")}" 320 | LOG.info '' 321 | 322 | final testSourceSet = androidExtension.sourceSets.getByName(test) 323 | final androidTestSourceSet = androidExtension.sourceSets.getByName(androidTest) 324 | androidExtension.sourceSets.each { sourceSet -> // androidTest, test, main 325 | LOG.info "$NAME_PLUGIN <<<===>>> sourceSet.name:${sourceSet.name}, sourceSet:$sourceSet" 326 | 327 | ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// 328 | // 实测发现:`sourceSet.name`和`variant.name`有些不同: 329 | // 例如:`sourceSet.name`是`androidTestGithubDebug`,而`variant.name`是`githubDebugAndroidTest`。但已有的`compileXxxJava/Kotlin` task 名字是以`variant.name`为准的。 330 | // 所以需要统一。 331 | LOG.info "$NAME_PLUGIN ---> contains:${variantsNames.contains(sourceSet.name)}" 332 | final srcSetNameMatchVariant = adjustSourceSetNameToMatchVariant(sourceSet.name, variantsNames) 333 | if (srcSetNameMatchVariant) { 334 | variantSourceSetMap.put(srcSetNameMatchVariant, sourceSet) 335 | 336 | // 此处不处理名字直接为以下的(但有名字如`xxxAndroidTest`的) 337 | if (![main, test, androidTest, unitTest].any { it == srcSetNameMatchVariant }) { 338 | final isTest = maybeIsTest(srcSetNameMatchVariant) 339 | final isAndroidTest = [androidTest, androidTest.capitalize()].any { srcSetNameMatchVariant.contains(it) } 340 | //ScalaRuntime scalaRuntime = project.extensions.getByName(SCALA_RUNTIME_EXTENSION_NAME) 341 | 342 | configureScalaCompile(project, sourceSet, mainSourceSet, androidTestSourceSet, testSourceSet, androidExtension, srcSetNameMatchVariant, isLibrary, isTest, isAndroidTest) 343 | } 344 | } 345 | } 346 | 347 | LOG.info "||||||||| |||||||||| |||||||||| link all variants depends on |||||||||| |||||||||| ||||||||||" 348 | def possibleVariant 349 | variantsAll.each { variant -> 350 | //variant = "$flavor$buildType" // e.g. "googleplayRelease", "githubDebug". 351 | //"compile${variant.name.capitalize()}Scala" // `.capitalize()`首字母大写 352 | LOG.info '' 353 | 354 | final isTest = maybeIsTest(variant.name) 355 | final isAndroidTest = [androidTest, androidTest.capitalize()].any { variant.name.contains(it) } 356 | if (!possibleVariant && !isTest) possibleVariant = variant 357 | final sourceSet = variantSourceSetMap[variant.name] 358 | 359 | linkScalaCompileDependsOn(project, scalroid, androidPlugin, androidExtension, workDir, variant, sourceSet, isLibrary, isTest, isAndroidTest) 360 | 361 | LOG.info "<<<<<<<<<<<============ ${variant.name} DONE ===============>>>>>>>>>>>>>>>>>>>" 362 | } 363 | 364 | ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// 365 | // 这一步需要等上面`linkScalaCompileDependsOn()`触发`ScalaCompile`配置完成,才能继续。 366 | // 但是没有名为`main`的 variant,也就没能正确`linkScalaCompileDependsOn()`,所以要找个`possibleVariant`。 367 | configureScaladocAndIncrementalElements(project, mainSourceSet, possibleVariant, isLibrary) 368 | } 369 | } 370 | 371 | void configureScaladocAndIncrementalElements(Project project, mainSourceSet, possibleVariant, boolean isLibrary) { 372 | // final mainScalaTaskName = genScalaCompileTaskName(possibleVariant.name) // mainSourceSet.name 373 | // final TaskProvider compileScala = project.tasks.withType(ScalaCompile).named(mainScalaTaskName) 374 | // final mainScalaCompile = compileScala.get() 375 | // 逻辑上,如果配置也是每个 variant 都配置。但这里没必要(针对依赖 subproject 的情况,必然也配置了 attribute,而且是输出)。 376 | //project.configurations.incrementalScalaAnalysisElements.outgoing.artifact(mainScalaCompile.analysisMappingFile) { builtBy(compileScala) } 377 | 378 | // 以上跟配置 scaladoc 也没关系,只不过`ScalaPlugin`把这两者写在一起了。 379 | // 而由于不是一套体系,执行 scaladoc 任务报错,暂禁言了。 380 | // configureScaladoc(project, mainSourceSet, mainScalaCompile) 381 | } 382 | 383 | // 把 scalroid 加入到 android 下面。可以这样写: 384 | // android { 385 | // scalroid { 386 | // message = 'Hi' 387 | // greeter = 'Gradle' 388 | // } 389 | // } 390 | private void addPluginExtensionToAndroidClosure(android, scalroid) { 391 | android.convention.plugins.put(NAME_PLUGIN, scalroid) // 正规的写法 392 | // 等同于(详见`InvokerHelper.invokeMethod()`): 393 | // `android.metaClass."get${NAME_PLUGIN.capitalize()}" = scalroid` 394 | android.metaClass."$NAME_PLUGIN" = scalroid // 加这个的原因同下面 395 | } 396 | 397 | //scalroid { 398 | // scala.zincVersion = '1.3.5' 399 | // ... 400 | //} 401 | private void addScalaPluginExtensionToScalroidClosure(scalroid, scala) { 402 | scalroid.convention.plugins.put(NAME_SCALA_EXTENSION, scala) // 正规的写法 403 | // 但如果不加这个,没法这样写(`build.gradle`和这里的代码都一样): 404 | // `scalroid.scala.zincVersion = xxx` 405 | // 只能这样: 406 | // scalroid { 407 | // scala.zincVersion.set(xxx) 408 | // } 409 | scalroid.metaClass."$NAME_SCALA_EXTENSION" = scala 410 | } 411 | 412 | // [evaluated] 表示是否在`project.afterEvaluate{}`中执行。 413 | private boolean resolveScalaSrcDirsToAndroidSourceSetsClosure(Project project, sourceSet, boolean evaluated) { 414 | LOG.info "$NAME_PLUGIN ---> [resolveScalaSrcDirsToAndroidSourceSetsClosure]sourceSet.name:${sourceSet.name}, displayName:${sourceSet.displayName}" 415 | LOG.info "$NAME_PLUGIN ---> [resolveScalaSrcDirsToAndroidSourceSetsClosure]sourceSet.extensions:${sourceSet.extensions}" 416 | //org.gradle.internal.extensibility.DefaultConvention 417 | 418 | // 对于不同的`sourceSet`,第一次肯定没有值。 419 | if (sourceSet.extensions.findByName('scala')) return false 420 | 421 | final displayName = sourceSet.displayName //(String) InvokerHelper.invokeMethod(sourceSet, "getDisplayName", null) 422 | 423 | SourceDirectorySet scalaDirSet 424 | final gradleVersion = project.gradle.gradleVersion 425 | final int verMajor = Integer.valueOf(gradleVersion.substring(0, gradleVersion.indexOf('.'))) 426 | 427 | // TODO: 俩版本不能通用,会报错,只能在 publish 某版本时手动注释掉另一 case。 428 | if (verMajor >= 8) { 429 | final scalaSourceSet = factory.newInstance(DefaultScalaSourceSet, displayName, factory) 430 | sourceSet.convention.plugins.put('scala', scalaSourceSet) 431 | scalaDirSet = scalaSourceSet.scala 432 | 433 | // TODO: 如果上面的不能用了,就启用下面的。 434 | // final ScalaSourceDirectorySet scala = factory.newInstance(DefaultScalaSourceDirectorySet, factory.sourceDirectorySet('scala', "${displayName} Scala source"), dependencyFactory) 435 | // scala.filter.include('**/*.java', '**/*.scala') 436 | // scalaDirSet = scala 437 | } else { 438 | //Convention sourceSetConvention = sourceSet.convention //(Convention) InvokerHelper.getProperty(sourceSet, "convention") 439 | final scalaSourceSet = new DefaultScalaSourceSet(displayName, factory) {} 440 | // final scalaSourceSet = factory.newInstance(DefaultScalaSourceSet, displayName, factory) 441 | // 这句`约定(convention)`的作用是添加: 442 | // sourceSets { main { scala.srcDirs += ['src/main/java'] } ...} 443 | sourceSet.convention.plugins.put('scala', scalaSourceSet) 444 | scalaDirSet = scalaSourceSet.scala 445 | } 446 | sourceSet.extensions.add(ScalaSourceDirectorySet, 'scala', scalaDirSet) 447 | scalaDirSet.srcDir(project.file("src/${sourceSet.name}/scala")) 448 | 449 | // Explicitly capture only a FileCollection in the lambda below for compatibility with configuration-cache. 450 | FileCollection scalaSource = scalaDirSet 451 | sourceSet.resources.filter.exclude(spec(element -> scalaSource.contains(element.file))) 452 | return true 453 | } 454 | 455 | private void configureScalaCompile(Project project, sourceSet, mainSourceSet, androidTestSourceSet, testSourceSet, androidExtension, String src$vaName, boolean isLibrary, boolean isTest, boolean isAndroidTest) { 456 | assert sourceSet != mainSourceSet && sourceSet != androidTestSourceSet && sourceSet != testSourceSet && mainSourceSet != androidTestSourceSet && mainSourceSet != testSourceSet && androidTestSourceSet != testSourceSet 457 | //printConfiguration(project, sourceSet.implementationConfigurationName) 458 | 459 | // 这一句通常如下(即在`main`里面),因此必须要加入`classpathConfigs`中(更多信息参见`ScalaRuntime.inferScalaClasspath()`): 460 | // implementation "org.scala-lang:scala-library:$scala2.Version" 461 | final Configuration mainClasspath = project.configurations.getByName(mainSourceSet.implementationConfigurationName) 462 | final Configuration mainCompileOnly = project.configurations.getByName(mainSourceSet.compileOnlyConfigurationName) 463 | final Configuration mainRuntimeOnly = project.configurations.getByName(mainSourceSet.runtimeOnlyConfigurationName) 464 | def classpathConfigs = mainClasspath.hierarchy + mainCompileOnly.hierarchy + mainRuntimeOnly.hierarchy 465 | 466 | final Configuration classpathByImplement = project.configurations.getByName(sourceSet.implementationConfigurationName) 467 | final Configuration compileOnlySourceSet = project.configurations.getByName(sourceSet.compileOnlyConfigurationName) 468 | final Configuration runtimeOnlySourceSet = project.configurations.getByName(sourceSet.runtimeOnlyConfigurationName) 469 | classpathConfigs += classpathByImplement.hierarchy + compileOnlySourceSet.hierarchy + runtimeOnlySourceSet.hierarchy 470 | if (isTest) { 471 | final Configuration implementationTest = project.configurations.getByName(testSourceSet.implementationConfigurationName) 472 | final Configuration compileOnlyTestSet = project.configurations.getByName(testSourceSet.compileOnlyConfigurationName) 473 | final Configuration runtimeOnlyTestSet = project.configurations.getByName(testSourceSet.runtimeOnlyConfigurationName) 474 | classpathConfigs += implementationTest.hierarchy + compileOnlyTestSet.hierarchy + runtimeOnlyTestSet.hierarchy 475 | if (isAndroidTest) { 476 | final Configuration implementationAndroidTest = project.configurations.getByName(androidTestSourceSet.implementationConfigurationName) 477 | final Configuration compileOnlyAndroidTestSet = project.configurations.getByName(androidTestSourceSet.compileOnlyConfigurationName) 478 | final Configuration runtimeOnlyAndroidTestSet = project.configurations.getByName(androidTestSourceSet.runtimeOnlyConfigurationName) 479 | classpathConfigs += implementationAndroidTest.hierarchy + compileOnlyAndroidTestSet.hierarchy + runtimeOnlyAndroidTestSet.hierarchy 480 | } 481 | } 482 | //classpathConfigs.removeIf { !it || it.dependencies.isEmpty() } // 在某些情况下,有可能不是`.isEmpty()`,如:`testImplementation`。所以不用去掉。 483 | // classpathConfigs = classpathConfigs.toSet() 484 | LOG.info "$NAME_PLUGIN ---> [configureScalaCompile]sourceSet.name:${sourceSet.name}, classpathConfigs.size:${classpathConfigs.size()}" 485 | 486 | // TODO: 注释掉的原因见下面`scalaCompile.analysisMappingFile`的注释 487 | // final incrementalAnalysisUsage = factory.named(Usage, "incremental-analysis") 488 | // Configuration incrementalAnalysis = project.configurations.create("incrementalScalaAnalysisFor${src$vaName.capitalize()}") 489 | // incrementalAnalysis.description = "Incremental compilation analysis input files for ${src$vaName}." 490 | // incrementalAnalysis.visible = false 491 | // incrementalAnalysis.canBeResolved = true 492 | // incrementalAnalysis.canBeConsumed = false 493 | // incrementalAnalysis.extendsFrom = classpathConfigs 494 | // incrementalAnalysis.attributes.attribute(Usage.USAGE_ATTRIBUTE, incrementalAnalysisUsage) 495 | 496 | // 若启用,则要分`${src$vaName}`,`incrementalAnalysisUsage`也要分`${src$vaName}`。 497 | // Configuration incrementalAnalysisElements = project.configurations.create("incrementalScalaAnalysisElements") 498 | // incrementalAnalysisElements.setDescription("Incremental compilation analysis output files for ${src$vaName}.") 499 | // incrementalAnalysisElements.setVisible(false) 500 | // incrementalAnalysisElements.setCanBeResolved(false) // 注意这里与上面的区别。这里 subproject `publishCode()`,上面 resolve,即拉取依赖的 subproject 产出的工件(artifacts)。 501 | // incrementalAnalysisElements.setCanBeConsumed(true) 502 | // incrementalAnalysisElements.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, incrementalAnalysisUsage) 503 | // AttributeMatchingStrategy matchingStrategy = dependencyHandler.getAttributesSchema().attribute(Usage.USAGE_ATTRIBUTE) 504 | // matchingStrategy.getDisambiguationRules().add(ScalaBasePlugin.UsageDisambiguationRules.class, (actionConfiguration) -> { 505 | // actionConfiguration.params(new Object[]{incrementalAnalysisUsage}) 506 | // actionConfiguration.params(new Object[]{this.objectFactory.named(Usage.class, "java-api")}) 507 | // actionConfiguration.params(new Object[]{this.objectFactory.named(Usage.class, "java-runtime")}) 508 | // }) 509 | 510 | final scalaCompileTask = project.tasks.register(genScalaCompileTaskName(src$vaName), ScalaCompile) { ScalaCompile scalaCompile -> 511 | LOG.info "$NAME_PLUGIN ---> [configureScalaCompile]compileTaskName:${scalaCompile.name}, isLibrary:$isLibrary, isTest:$isTest, isAndroidTest:$isAndroidTest" 512 | 513 | final compilerClasspath = project.configurations.create("${src$vaName}ScalaCompileClasspath").setExtendsFrom(classpathConfigs) 514 | 515 | scalaCompile.classpath = obtainWithSetsOnlyCanBeResolvedVariantSelectionAttributes(project, scalaCompile, compilerClasspath, src$vaName, isTest) 516 | // TODO: 实测要把`android.jar`也加入 classpath,否则如果 scala 代码间接(或直接)引用如`android.app.Activity`,会报如下错误: 517 | // [Error] /Users/.../demo-material-3/app/src/main/scala/com/example/demomaterial3/Test2.scala:9:7: Class android.app.Activity not found - continuing with a stub. one error found... 518 | scalaCompile.classpath += androidExtension.bootClasspathConfig.mockableJarArtifact // mock `android.jar` 519 | 520 | ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// 521 | final ScalaSourceDirectorySet scalaDirectorySet = sourceSet.extensions.getByType(ScalaSourceDirectorySet) 522 | scalaCompile.source(scalaDirectorySet) 523 | if (isTest) { 524 | // 但要加入到 classpath(实测没必要) 525 | //scalaCompile.classpath += project.files(project.layout.buildDirectory.dir("tmp/scala-classes/${parseSimpleVariantNameForTest(src$vaName)}")) 526 | if (isAndroidTest) { 527 | final ScalaSourceDirectorySet androidTestDirSet = androidTestSourceSet.extensions.getByType(ScalaSourceDirectorySet) 528 | if (androidTestDirSet) scalaCompile.source(androidTestDirSet) 529 | } else { 530 | final ScalaSourceDirectorySet testDirectorySet = testSourceSet.extensions.getByType(ScalaSourceDirectorySet) 531 | if (testDirectorySet) scalaCompile.source(testDirectorySet) 532 | } 533 | } else { 534 | // TODO: 不编译的终极原因(`./gradlew :app:compileGithubDebugScala --info`): 535 | // Skipping task ':app:compileGithubDebugScala' as it has no source files and no previous output files. 536 | final ScalaSourceDirectorySet mainScalaDirSet = mainSourceSet.extensions.getByType(ScalaSourceDirectorySet) 537 | // 另外,`!isTest`的原因: 538 | // `:app:compile{variant}AndroidTestJavaWithJavac`本来就依赖于`:app:compile{variant}JavaWithJavac`,所以就不用在这里手动加了。 539 | scalaCompile.source(mainScalaDirSet) 540 | 541 | // 没必要,下面`scalaCompile.dependsOn xxx`有。这里主要是为了源码引用不标红,但不起作用。 542 | /*scalaCompile.source(project.layout.buildDirectory.dir('generated').flatMap(new Transformer, Directory>() { 543 | @Override 544 | Provider transform(Directory dir) { 545 | return project.provider(new Callable() { 546 | @Override 547 | FileCollection call() throws Exception { 548 | return dir.asFileTree.filter { File f -> f.path.endsWith('.java') && f.path.contains(src$vaName) } 549 | } 550 | }) 551 | } 552 | }))*/ 553 | } 554 | ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// 555 | 556 | scalaCompile.description = "Compiles Scala code for variant ${src$vaName}." 557 | //scalaCompile.javaLauncher.convention(getToolchainTool(project, JavaToolchainService::launcherFor)) 558 | 559 | // TODO: 没用,只有同时配置了`incrementalOptions.publishedCode`这行才能被调用(但也没必要,会被后续其它后续任务`publish`,针对 560 | // 依赖 subproject 的情况)。同时,是否是否禁用增量编译,仅需看是否存在`incrementalOptions.analysisFile`。 561 | // 参见`scalaCompile.compile()` -> `isNonIncrementalCompilation()`。 562 | scalaCompile.analysisMappingFile.set(project.layout.buildDirectory.file("scala/compilerAnalysis/${src$vaName}.mapping")) 563 | 564 | // Cannot compute at task execution time because we need association with source set 565 | IncrementalCompileOptions incrementalOptions = scalaCompile.scalaCompileOptions.incrementalOptions 566 | incrementalOptions.analysisFile.set(project.layout.buildDirectory.file("scala/compilerAnalysis/${src$vaName}.analysis")) 567 | incrementalOptions.classfileBackupDir.set(project.layout.buildDirectory.file("scala/classfileBackup/${src$vaName}.bak")) 568 | 569 | // scalaCompile.analysisFiles.from(incrementalAnalysis.incoming.artifactView { 570 | // lenient(true) 571 | // componentFilter(new Spec() { 572 | // boolean isSatisfiedBy(ComponentIdentifier element) { return element instanceof ProjectComponentIdentifier } 573 | // }) 574 | // }.files) 575 | scalaCompile.dependsOn(scalaCompile.analysisFiles) 576 | 577 | // 目录与 kotlin 保持一致(原本下面要用到,但没用,已删)。 578 | //scalaDirectorySet.destinationDirectory.convention(project.layout.buildDirectory.dir("tmp/scala-classes/pre${src$vaName.capitalize()}Roughly")) 579 | scalaCompile.destinationDirectory.convention(/*scalaDirectorySet.destinationDirectory*/ project.layout.buildDirectory.dir("tmp/scala-classes/pre${src$vaName.capitalize()}Roughly")) 580 | } 581 | 582 | // 定义一个去重任务(由于在`scalaCompile.doLast{}`中删除某些文件会影响【增量编译】) 583 | project.tasks.register(genDeduplicateClassesTaskName(src$vaName), ScalaDeDuplicateClassesTask) { ScalaDeDuplicateClassesTask classesTask -> 584 | final scalaCompile = scalaCompileTask.get() 585 | 586 | classesTask.inputDir.convention(scalaCompile.destinationDirectory) 587 | classesTask.outputDir.convention(project.layout.buildDirectory.dir("tmp/scala-classes/${src$vaName}")) 588 | 589 | classesTask.dependsOn scalaCompile 590 | 591 | classesTask.LOG.set(LOG) 592 | classesTask.NAME_PLUGIN.set(NAME_PLUGIN) 593 | } 594 | } 595 | 596 | private Configuration obtainWithSetsOnlyCanBeResolvedVariantSelectionAttributes(Project project, Task scalaCompile, Configuration compilerClasspath, String src$vaName, boolean isTest) { 597 | //printClasspathOnTaskDoFirst(project, scalaCompile, compilerClasspath, src$vaName, null) 598 | 599 | // 详情参见: 600 | // https://docs.gradle.org/current/userguide/variant_model.html#sec:variant-visual 601 | // https://docs.gradle.org/current/userguide/variant_attributes.html#sec:abm_algorithm 602 | // 之前卡了几周的问题在这: 603 | // https://docs.gradle.org/current/userguide/declaring_dependencies.html#sec:resolvable-consumable-configs 604 | // https://docs.gradle.org/current/userguide/variant_attributes.html#sec:abm_disambiguation_rules 605 | // 由于一直不知道这两个属性起什么作用,从而被忽略。但默认值均为 true,导致: 606 | // 1. 既可以解析依赖(即 consumer 角色,根据设置的 attributes 选择依赖的 variant); 607 | // 2. 又可以被消化,也就是候选者(candidate)。 608 | // 由于下面设置的 attributes 无法区分上游的被依赖者是哪个 project,所以会总是设置了兼容的 attributes 从而 609 | // 被上游错误地解析到。所以要么出现歧义(disambiguation),要么编译时出现错误。歧义问题表现在依赖的 sub-project,在 610 | // api project(path: ':annoid', configuration: 'debugRuntimeElements') 611 | // 配置中,configuration 通常不设置,即 612 | // api project(':annoid') 613 | // 默认就是`default`,等同于 614 | // api project(path: ':annoid', configuration: 'default') 615 | // 就会报错。解决方法及其简单: 616 | compilerClasspath.canBeResolved = true 617 | compilerClasspath.canBeConsumed = false 618 | /* 619 | // 运行`./gradlew :annoid:outgoingVariants`可以看到: 620 | // (`./gradlew :assoid:resolvableConfigurations`) 621 | 622 | > Task :annoid:outgoingVariants 623 | -------------------------------------------------- 624 | Variant debugApiElements // 有很多 625 | -------------------------------------------------- 626 | API elements for debug 627 | 628 | Capabilities 629 | - hobby.wei.c.anno:annoid:1.2.1 (default capability) 630 | Attributes 631 | - com.android.build.api.attributes.AgpVersionAttr = 7.4.0 632 | - com.android.build.api.attributes.BuildTypeAttr = debug 633 | - com.android.build.gradle.internal.attributes.VariantAttr = debug 634 | - org.gradle.category = library 635 | - org.gradle.jvm.environment = android 636 | - org.gradle.usage = java-api 637 | - org.jetbrains.kotlin.platform.type = androidJvm 638 | 639 | Secondary Variants (*) 640 | 641 | -------------------------------------------------- 642 | Secondary Variant android-aidl 643 | -------------------------------------------------- 644 | ... 645 | 646 | -------------------------------------------------- 647 | Secondary Variant android-classes-jar 648 | -------------------------------------------------- 649 | Attributes 650 | - ... 651 | Artifacts 652 | - build/intermediates/compile_library_classes_jar/debug/classes.jar (artifactType = android-classes-jar) 653 | */ 654 | // TODO: 655 | // 这里只需要把重点区别标识出来,也就是 Secondary Variants,有很多。这里需要的是 656 | // `artifactType = android-classes-jar`,还要个 657 | // `com.android.build.api.attributes.BuildTypeAttr = debug/release` 658 | // 即可。 659 | 660 | //final attrUsage = 'org.gradle.usage' // java-runtime/java-api 661 | //final attrCategory = 'org.gradle.category' // library 662 | //final attrJvmEnv = '.jvm.environment' // android 663 | //final attrPlatType = '.platform.type' // androidJvm 664 | final attrBuildType = '.attributes.BuildType' // debug/release 665 | //final attrVariant = '.attributes.Variant' // debug/release/xxxDebug/xxxRelease 666 | //final attrAgpVersion = '.attributes.AgpVersion' // 7.4.0 667 | //final attrLibElements = LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE // 'org.gradle.libraryelements' // classes 668 | final List forceAttrs = [attrBuildType] //attrUsage, attrCategory, attrJvmEnv, attrPlatType] 669 | 670 | final CLASSES_JAR = 'android-classes-jar' 671 | final artifactType = ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE 672 | //compilerClasspath.attributes.attribute(USAGE_ATTRIBUTE, factory.named(Usage, JAVA_RUNTIME)) 673 | //compilerClasspath.attributes.attribute(CATEGORY_ATTRIBUTE, factory.named(Category, LIBRARY)) 674 | // com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.CLASSES_JAR.type/AAR_OR_JAR.type 675 | compilerClasspath.attributes.attribute(artifactType, CLASSES_JAR) 676 | 677 | //final isDebug = [debug, debug.capitalize()].any { src$vaName.contains(it) } 678 | final apiElements = project.configurations.findByName("${isTest ? parseSimpleVariantNameForTest(src$vaName) : src$vaName}ApiElements") 679 | final runtimeElements = project.configurations.findByName("${isTest ? parseSimpleVariantNameForTest(src$vaName) : src$vaName}RuntimeElements") 680 | final elements = runtimeElements ? runtimeElements : apiElements 681 | elements.attributes.each { attrs -> 682 | attrs.keySet().each { attr -> 683 | final value = attrs.getAttribute(attr) 684 | LOG.info "$NAME_PLUGIN ---> [setClasspathAttributes]attribute > ${attr.name} -> ${value} / ${value.class}" 685 | if (forceAttrs.any { attr.name.contains(it) }) { 686 | LOG.info "$NAME_PLUGIN ---> [setClasspathAttributes]accept > ${attr.name} -> ${value}" 687 | compilerClasspath.attributes.attribute(attr, value) 688 | } 689 | } 690 | } 691 | return compilerClasspath 692 | } 693 | 694 | private void linkScalaCompileDependsOn(Project project, ScalroidExtension scalroid, Plugin androidPlugin, androidExtension, File workDir, variant, sourceSet, boolean isLibrary, boolean isTest, boolean isAndroidTest) { 695 | //LOG.info "$NAME_PLUGIN ---> [linkScalaCompileDependsOn]androidPlugin:${androidPlugin}" // com.android.build.gradle.AppPlugin@8ab260a 696 | //LOG.info "$NAME_PLUGIN ---> [linkScalaCompileDependsOn]workDir:${workDir.path}" // /Users/{PATH-TO-}/demo-material-3/app/build/scalroid 697 | LOG.info "$NAME_PLUGIN ---> [linkScalaCompileDependsOn]variant:${variant.name}" // githubDebug 698 | 699 | final javaTaskName = genJavaCompileTaskName(variant.name) 700 | final scalaTaskName = genScalaCompileTaskName(variant.name) 701 | final scalaDeduplicateName = genDeduplicateClassesTaskName(variant.name) 702 | final kotlinTaskName = genKotlinCompileTaskName(variant.name) 703 | final buildConfigTaskName = genBuildConfigTaskName(variant.name) 704 | final processResourcesTaskName = genProcessResourcesTaskName(variant.name) 705 | final rFileTaskName = genRFileTaskName(variant.name) 706 | final dataBindingGenBaseTaskName = genDataBindingGenBaseTaskName(variant.name) 707 | 708 | final JavaCompile javaCompile = project.tasks.findByName(javaTaskName) 709 | if (javaCompile) { 710 | // 获取前面已经注册的`scalaCompileTask`。见`project.tasks.register()`文档。 711 | project.tasks.withType(ScalaCompile).getByName(scalaTaskName) { ScalaCompile scalaCompile -> 712 | LOG.info "$NAME_PLUGIN ---> [linkScalaCompileDependsOn]javaCompile.destinationDirectory:${javaCompile.destinationDirectory.orNull}" 713 | 714 | final scalaDeduplicate = project.tasks.withType(ScalaDeDuplicateClassesTask).getByName(scalaDeduplicateName) 715 | //javaCompile.dependsOn scalaCompile 716 | javaCompile.dependsOn scalaDeduplicate 717 | 718 | // 目录与 kotlin 保持一致(前面已经设置默认值了) 719 | //scalaCompile.destinationDirectory.set(project.layout.buildDirectory.dir("tmp/scala-classes/pre${variant.name.capitalize()}Roughly")) 720 | scalaCompile.sourceCompatibility = javaCompile.sourceCompatibility 721 | scalaCompile.targetCompatibility = javaCompile.targetCompatibility 722 | // Unexpected javac output: 警告: [options] 未与 -source 8 一起设置引导类路径 723 | scalaCompile.options.bootstrapClasspath = javaCompile.options.bootstrapClasspath 724 | // TODO: Unexpected javac output: 警告: [options] 未与 -source 11 一起设置系统模块路径 725 | // 原因是`app/build.gradle`的以下设置不正确,应与当前使用的`jdk`版本保持一致。例如: 726 | // `org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17-aarch64.jdk/Contents/Home` 727 | /*compileOptions { 728 | sourceCompatibility JavaVersion.VERSION_17 729 | targetCompatibility JavaVersion.VERSION_17 730 | } 731 | kotlinOptions { jvmTarget = '17' } // 有了下面的,这行设置可以不要。 732 | kotlin { 733 | jvmToolchain(17) 734 | }*/ 735 | scalaCompile.options.sourcepath = javaCompile.options.sourcepath 736 | scalaCompile.options.annotationProcessorPath = javaCompile.options.annotationProcessorPath 737 | scalaCompile.options.encoding = javaCompile.options.encoding 738 | 739 | // scalaCompile.scalaCompileOptions 可以在`build.gradle`脚本中配置 740 | //scalaCompile.scalaCompileOptions.encoding = scalaCompile.options.encoding 741 | 742 | project.tasks.getByName(kotlinTaskName) { kotlinCompile -> 743 | //LOG.info "$NAME_PLUGIN ---> [linkScalaCompileDependsOn]kotlinCompile:${kotlinCompile} / ${kotlinCompile.class}" // org.jetbrains.kotlin.gradle.tasks.KotlinCompile 744 | wireScalaTasks(project, scalroid, variant, project.tasks.named(scalaTaskName), project.tasks.named(scalaDeduplicateName), project.tasks.named(javaTaskName), project.tasks.named(kotlinTaskName), isLibrary, isTest) 745 | } 746 | // `:app:compile{variant}AndroidTestJavaWithJavac`本来就依赖于`:app:compile{variant}JavaWithJavac`,而 747 | // `:app:compile{variant}JavaWithJavac`依赖于`:app:compile{variant}Scala`,所以就不用在这里手动加了(加也无妨)。 748 | if (isTest) { 749 | LOG.info "$NAME_PLUGIN ---> [linkScalaCompileDependsOn]isTest:$isTest" 750 | /*final scalaMainVarTaskName = genScalaCompileTaskName(parseSimpleVariantNameForTest(variant.name)) 751 | project.tasks.withType(ScalaCompile).getByName(scalaMainVarTaskName) { ScalaCompile scalaCompileSimple -> 752 | LOG.info "$NAME_PLUGIN ---> [linkScalaCompileDependsOn]scalaCompileSimple.name:${scalaCompileSimple.name}" 753 | scalaCompile.dependsOn scalaCompileSimple 754 | }*/ 755 | final scalaMainVarTaskName = genDeduplicateClassesTaskName(parseSimpleVariantNameForTest(variant.name)) 756 | project.tasks.getByName(scalaMainVarTaskName) { deduplicateClasses -> 757 | LOG.info "$NAME_PLUGIN ---> [linkScalaCompileDependsOn]deduplicateClasses.name:${deduplicateClasses.name}" 758 | scalaCompile.dependsOn deduplicateClasses 759 | } 760 | evictCompileOutputForSrcTask(scalaDeduplicate, project, scalaCompile, null, scalroid, 761 | "src/${isAndroidTest ? androidTest : test}/java/", "src/${isAndroidTest ? androidTest : test}/kotlin/", 762 | "src/${sourceSet.name}/java/", "src/${sourceSet.name}/kotlin/") 763 | } else { 764 | final processRes = project.tasks.findByName(processResourcesTaskName) 765 | final rFile = project.tasks.findByName(rFileTaskName) 766 | final dataBinding = project.tasks.findByName(dataBindingGenBaseTaskName) 767 | if (processRes) { 768 | final files = project.files(processRes.outputs.files.find { it.path.endsWith("${variant.name}/R.jar") }) 769 | scalaCompile.dependsOn processRes 770 | scalaCompile.classpath += files 771 | 772 | if (scalroid.setAppRJarAsLib.get()) { 773 | assert !isLibrary // `processRes`存在就说明是主`app`,不是 lib。 774 | // 等同于`build.gradle`中的: 775 | // ${variant.name}CompileOnly files('build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/${variant.name}/R.jar') 776 | project.dependencies.add("${variant.name}CompileOnly", files) 777 | } 778 | } else if (rFile) { 779 | scalaCompile.dependsOn rFile 780 | scalaCompile.classpath += project.files(rFile.outputs.files.find { it.path.endsWith("${variant.name}/R.jar") }) 781 | } 782 | // 把生成的`BuildConfig`加入依赖。同理,还可以加入别的。 783 | project.tasks.named(buildConfigTaskName) { Task buildConfig -> // ... 784 | scalaCompile.source(buildConfig) 785 | evictCompileOutputForSrcTask(scalaDeduplicate, project, scalaCompile, buildConfig, scalroid, "src/${main}/java/", "src/${main}/kotlin/", "src/${sourceSet.name}/java/", "src/${sourceSet.name}/kotlin/") 786 | } 787 | if (dataBinding) { 788 | scalaCompile.source(dataBinding.outputs.files.filter { it.path.contains("/generated/") && it.path.contains("/${variant.name}/") }) 789 | evictCompileOutputForSrcTask(scalaDeduplicate, project, scalaCompile, dataBinding, null /*置为 null,避免重复计算。*/) 790 | } 791 | } 792 | } 793 | } 794 | } 795 | 796 | private void evictCompileOutputForSrcTask(ScalaDeDuplicateClassesTask deduplicate, Project project, AbstractCompile dest, @Nullable Task src, @Nullable ScalroidExtension scalroid, String... srcDirs) { 797 | // 无法创建针对输出的过滤器(而输入默认携带`task.exclude()`),试过所以方法(包括搜索文档)。因为`task.outputs.xxx`基本都是不可变的,意味着 798 | // 调用某的方法仅返回一个值,无法设置进去从而起作用。 799 | //PatternFilterable patternFilter = patternSetFactory.create() 800 | //scalaCompile.outputs.files.asFileTree.matching(patternFilter) 801 | /*dest.doFirst { 802 | src.outputs.files.files.each { 803 | println "[src.outputs] ??? | $it" 804 | } 805 | dest.source.files.each { 806 | println "[dest.source] == | $it" 807 | } 808 | }*/ 809 | final parentPaths = [] 810 | final excludePaths = [] 811 | final parentPathToLens = [:] 812 | dest.doFirst { 813 | if (src) { 814 | parentPaths.addAll(src.outputs.files.files.findAll { it.isDirectory() }.collect { it.path }.sort { (o1, o2) -> o1.length() - o2.length() }.toSet().toList()) 815 | final temp = []; temp.addAll(parentPaths) 816 | for (int i = 0; i < temp.size(); i++) { 817 | for (int j = i + 1; j < temp.size(); j++) { // 由于已经排序了,后面的比较长。 818 | if (temp[j].startsWith(temp[i])) parentPaths.remove(temp[j]) 819 | } 820 | } 821 | // 通过上述算法,`parentPaths`已经剩下每个不同目录的最短`目录的 path`。 822 | parentPaths.each { path -> parentPathToLens.put(path, path.length() + (path.endsWith('/') ? 0 : 1)) } 823 | LOG.info "$NAME_PLUGIN ---> [evictCompileOutputForSrcTask]parentPathToLens:${parentPathToLens}" 824 | } 825 | final tempDirs = []; if (scalroid) tempDirs.addAll(scalroid.javaDirsExcludes.get()); tempDirs.addAll(srcDirs) 826 | excludePaths.addAll(tempDirs.collect { it.endsWith('/') ? it : (it + '/') }.toSet().toList()) 827 | LOG.info "$NAME_PLUGIN ---> [evictCompileOutputForSrcTask]excludePaths:${excludePaths}" 828 | } 829 | dest.doLast { 830 | final destDir = dest.destinationDirectory.asFile.get().path 831 | final destLen = destDir.length() + (destDir.endsWith('/') ? 0 : 1) 832 | final projDir = project.layout.projectDirectory.asFile.path 833 | final projLen = projDir.length() + (projDir.endsWith('/') ? 0 : 1) 834 | 835 | final allSrcMatchTask = dest.source.files.findAll { file -> parentPaths.any { file.path.startsWith(it) } } 836 | final allSrcMatchDirs = dest.source.files.findAll { file -> excludePaths.any { file.path.indexOf(it, projLen) > 0 } } 837 | 838 | final pkgOrNamesEvicts = allSrcMatchTask 839 | .collect { file -> file.path.substring(parentPathToLens.find { file.path.startsWith(it.key) }.value) } 840 | .collect { final i = it.lastIndexOf('/'); final j = it.lastIndexOf('.'); i < j && j > 0 ? it.substring(0, j) : it } 841 | LOG.info "$NAME_PLUGIN ---> [evictCompileOutputForSrcTask]packageOrNamesEvicts:${pkgOrNamesEvicts}" 842 | 843 | final pkgOrNamesExcludes = allSrcMatchDirs 844 | .collect { file -> int i = -1; final path = excludePaths.find { i = file.path.indexOf(it, projLen); i > 0 }; assert i > 0; file.path.substring(i + path.length()) } 845 | .collect { final i = it.lastIndexOf('/'); final j = it.lastIndexOf('.'); i < j && j > 0 ? it.substring(0, j) : it } 846 | LOG.info "$NAME_PLUGIN ---> [evictCompileOutputForSrcTask]packageOrNamesExcludes:${pkgOrNamesExcludes}" 847 | 848 | deduplicate.packageOrNamesEvicts.addAll(pkgOrNamesEvicts) 849 | deduplicate.packageOrNamesExcludes.addAll(pkgOrNamesExcludes) 850 | 851 | /*dest.outputs.files.asFileTree.each { File file -> 852 | final hit = (pkgNamePaths + pkgNameExcludes).any { pkgName -> 853 | //file.path.substring(destLen) == (pkgName + '.class') // 可能后面跟的不是`.class`而是`$1.class`、`$xxx.class`。 854 | //file.path.substring(destLen).startsWith(pkgName) // 改写如下: 855 | file.path.indexOf(pkgName, destLen) == destLen && file.path.indexOf('/', destLen + pkgName.length()) < 0 856 | //hobby/wei/c/L$3.class 857 | //hobby/wei/c/LOG.class 858 | && (file.path.indexOf(pkgName + '.', destLen) == destLen || file.path.indexOf(pkgName + '$', destLen) == destLen) 859 | } 860 | if (hit) { 861 | LOG.info "$NAME_PLUGIN ---> [evictCompileOutputForSrcTask] ^^^ HIT:$file" 862 | file.delete() // TODO: 影响增量编译的问题在这,但这貌似只是影响极小的一个点。还有其它原因,还在搜寻中… 863 | } else { 864 | LOG.info "$NAME_PLUGIN ---> [evictCompileOutputForSrcTask] NOT HIT:$file" 865 | } 866 | }*/ 867 | } 868 | } 869 | 870 | private defaultSourceSet(Project project, variant) { 871 | // 就是各个 variant 对应的 SourceSet。 872 | // debugAndroidTest, debug, release 873 | // githubDebugAndroidTest, googleplayDebugAndroidTest, githubDebug, googleplayDebug, githubRelease, googleplayRelease 874 | return kotlinJvmAndroidCompilation(project, variant).defaultSourceSet 875 | } 876 | 877 | private kotlinAndroidTarget(Project project) { 878 | // 参见`org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPlugin`开头的`(project.kotlinExtension as KotlinAndroidProjectExtension).target = it` 879 | // 根据源码分析,这里已不需要进行`.castIsolatedKotlinPluginClassLoaderAware()`,也没法直接调用(其目的 880 | // 是过早地发现错误并给出详细的建议,见`IsolatedKotlinClasspathClassCastException`)。 881 | final kotlinExtension = project.extensions.getByName(NAME_KOTLIN_EXTENSION) // KotlinAndroidProjectExtension_Decorated 882 | return kotlinExtension.target // KotlinAndroidTarget 883 | } 884 | 885 | private kotlinJvmAndroidCompilation(Project project, variant) { 886 | return kotlinAndroidTarget(project).compilations.getByName(variant.name) // KotlinJvmAndroidCompilation 887 | } 888 | 889 | private void wireScalaTasks(Project project, ScalroidExtension scalroid, variant, TaskProvider scalaTask, TaskProvider deduplicate, TaskProvider javaTask, TaskProvider kotlinTask, boolean isLibrary, boolean isTest) { 890 | final compilation = kotlinJvmAndroidCompilation(project, variant) 891 | final outputs = compilation.output.classesDirs // ConfigurableFileCollection 892 | //LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]outputs:${outputs.class}, it.files:${outputs.files}" 893 | // outputs.from(scalaTask.flatMap { it.destinationDirectory }) 894 | outputs.from(deduplicate.flatMap { it.outputDir }) 895 | //final outputs1 = compilation.output.classesDirs 896 | //LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]outputs1:${outputs1.class}, it.files:${outputs1.files}" 897 | 898 | // 写法参见`org.jetbrains.kotlin.gradle.plugin.Android25ProjectHandler`的`wireKotlinTasks()`。 899 | // 如果这样写`project.files(scalaTask.get().destinationDirectory)`会导致 Task 的循环依赖。 900 | final javaOuts = project.files(project.provider([call: { javaTask.get().destinationDirectory.get().asFile }] as Callable)) 901 | final scalaOuts = project.files(project.provider([call: { scalaTask.get().destinationDirectory.get().asFile }] as Callable)) 902 | final scalaClasses = project.files(project.provider([call: { deduplicate.get().outputDir.get().asFile }] as Callable)) 903 | final kotlinOuts = project.files(project.provider([call: { kotlinTask.get().destinationDirectory.get().asFile }] as Callable)) 904 | LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]javaOuts:${javaOuts}, it.files:${javaOuts.files}" 905 | LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]scalaOuts:${scalaOuts}, it.files:${scalaOuts.files}" 906 | LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]scalaClasses:${scalaClasses}, it.files:${scalaClasses.files}" 907 | LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]kotlinOuts:${kotlinOuts}, it.files:${kotlinOuts.files}" 908 | 909 | // 这句的作用是便于分析出任务依赖关系(In order to properly wire up tasks),详见`Object registerPreJavacGeneratedBytecode(FileCollection)`文档。 910 | // 这句可以不要以欺骗依赖图生成循环依赖。 911 | // scalaOuts.builtBy(scalaTask) 912 | //variant.registerJavaGeneratingTask(scalaTask, scalaTask.get().source.files) 913 | //variant.registerJavaGeneratingTask(kotlinTask, kotlinTask.get().sources.files) 914 | 915 | //final outsNew = javaOuts + kotlinOuts //.from(kotlinOuts) 916 | //final classpathKey = variant.registerPreJavacGeneratedBytecode(outsNew) 917 | //LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]classpathKey:${classpathKey} / ${classpathKey.class}" // java.lang.Object@2faa3212 / class java.lang.Object 918 | 919 | // 根据根据源码分析,下面`classpathKey`可以不传(即:null)。 920 | // 但不传的话,拿到的`compileClasspath`不一样,具体表现在:`传/不传`的循环依赖不同。 921 | // 根据源码分析,拿到的返回值包含`variant.getGeneratedBytecode(classpathKey)`的返回值,该值包含 922 | // 参数`classpathKey`在注册(`variant.registerPreJavacGeneratedBytecode(fileCol)`)之前传入(即`fileCol`)的所有值。 923 | // 简单来说,每次调用下面的方法,返回值都包含[除本次注册外]之前注册时入参的所有`FileCollection`。 924 | // 而如果没有参数,则包含所有已注册的值。 925 | // final compileClasspath = variant.getCompileClasspath(/*classpathKey*/) 926 | // org.gradle.api.internal.file.collections.DefaultConfigurableFileCollection 927 | //LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]variant.getCompileClasspath(classpathKey):${compileClasspath} / ${compileClasspath.class}" 928 | // TODO: 929 | // java.lang.RuntimeException: Configuration 'githubDebugCompileClasspath' was resolved during configuration time. 930 | // This is a build performance and scalability issue. 931 | // scalroid ---> [wireScalaTasks]variant.getCompileClasspath(classpathKey):[/Users/weichou/git/bdo.cash/demo-material-3/app/build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/ 932 | // githubDebug/R.jar, /Users/weichou/.gradle/caches/transforms-3/893b9b5a0019ab2d13f957bcf2fabcb9/transformed/viewbinding-7.3.1-api.jar, /Users/weichou/.gradle/caches/transforms-3/3b9fa4710e9e16ecd783cc23f2a62967/transformed/navigation-ui-ktx-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/b4c5cd6f5d69a09c90e3ea53830c48f5/transformed/navigation-ui-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/7c5ae5c220196941122f9db4d7a639bc/transformed/material-1.7.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/a2622ad63284a4fbebfb584b9b851884/transformed/appcompat-1.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/6c3948d45ddaf709ba26745189eab999/transformed/viewpager2-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/2a8d540a7e825c31c133e1550351db89/transformed/navigation-fragment-ktx-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/d9907136fb1ec2be0470c0c515d25b44/transformed/navigation-fragment-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/6eaf265bb3918c4fd1dce0869a434f72/transformed/fragment-ktx-1.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/3ff5ce784625b6b2d8a5c8ed94aa647c/transformed/fragment-1.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/27c0c7f932da50fcd00c251ed8922e6d/transformed/navigation-runtime-ktx-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/b010a8c66f1d64f1d21c27bb1beb821c/transformed/navigation-runtime-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/f151321734e20d7116485bb3ab462df6/transformed/activity-ktx-1.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/21307ac95e78f6bab6ef32fc6c8c8597/transformed/activity-1.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/b7a04d7ac46f005b9d948413fa63b076/transformed/navigation-common-ktx-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/7a98affa0f1b253f31ac3690fb7b577b/transformed/navigation-common-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/49780de7b782ff560647221ad2cc9d58/transformed/lifecycle-viewmodel-savedstate-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/ec53663f906b415395ef3a78e7add5aa/transformed/lifecycle-viewmodel-ktx-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/498d61dce25ec5c15c1a4140b70f1b13/transformed/lifecycle-runtime-ktx-2.5.0-api.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/kotlinx-coroutines-core-jvm/1.6.1/97fd74ccf54a863d221956ffcd21835e168e2aaa/kotlinx-coroutines-core-jvm-1.6.1.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/kotlinx-coroutines-android/1.6.1/4e61fcdcc508cbaa37c4a284a50205d7c7767e37/kotlinx-coroutines-android-1.6.1.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.7.20/eac6656981d9d7156e838467d2d8d79093b1570/kotlin-stdlib-jdk8-1.7.20.jar, /Users/weichou/.gradle/caches/transforms-3/394f4ba5a7303202a56e467034c8c851/transformed/core-ktx-1.9.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/6e9fde4cc1497ecd85ffdf980a60e5ab/transformed/appcompat-resources-1.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/aa25fefefb4284675752d1b8716a8bf4/transformed/drawerlayout-1.1.1-api.jar, /Users/weichou/.gradle/caches/transforms-3/6f5d72cc112503558a75fc45f0fbfe22/transformed/coordinatorlayout-1.1.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/f66c58055f09c54df80d3ac39cfc8ed7/transformed/dynamicanimation-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/76fb2fc3976e6e2590393baaa768ea01/transformed/recyclerview-1.1.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/d5505f9d57d8c6a72c4bfbea8e0d1d59/transformed/transition-1.4.1-api.jar, /Users/weichou/.gradle/caches/transforms-3/cb376832b44347e0e2863a0efbfb46c1/transformed/vectordrawable-animated-1.1.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/51c160350e1db060225cf076555bddc5/transformed/vectordrawable-1.1.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/b437c042a4fb25b5473beb3aa3810946/transformed/viewpager-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/c711a0ee0692c4fae81cdd671dadbde0/transformed/slidingpanelayout-1.2.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/60367c0dc3b93ae22d44974ed438a5f5/transformed/customview-1.1.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/05f2d7edda9b38183e0acbcdee061d41/transformed/legacy-support-core-utils-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/5bb317b860d652a7e95da35400298e99/transformed/loader-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/0c3f5120a16030c3b02e209277f606e0/transformed/core-1.9.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/c9dedd1f96993d0760d55df081a29879/transformed/cursoradapter-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/8bb096c8444b515fe520f2a8e48caab1/transformed/savedstate-ktx-1.2.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/cdb42f31a8e9bad7a8ea34dbb5e7b7f6/transformed/savedstate-1.2.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/9c4ff74ae88f4d922542d59f88faa6bc/transformed/cardview-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/ee8da9fe5cd86ed12e7a623b8098a166/transformed/lifecycle-runtime-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/b81c0a66f40e81ce8db2df92a1963d5b/transformed/versionedparcelable-1.1.1-api.jar, /Users/weichou/.gradle/caches/transforms-3/83b5d525a0ebd9bc00322953f05c96f4/transformed/lifecycle-viewmodel-2.5.0-api.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/androidx.collection/collection-ktx/1.1.0/f807b2f366f7b75142a67d2f3c10031065b5168/collection-ktx-1.1.0.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/androidx.collection/collection/1.1.0/1f27220b47669781457de0d600849a5de0e89909/collection-1.1.0.jar, /Users/weichou/.gradle/caches/transforms-3/f306f739dc2177bf68711c65fe4e11e2/transformed/lifecycle-livedata-2.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/dfab4e028be7115b8ccc3796fb2e0428/transformed/core-runtime-2.1.0-api.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/androidx.arch.core/core-common/2.1.0/b3152fc64428c9354344bd89848ecddc09b6f07e/core-common-2.1.0.jar, /Users/weichou/.gradle/caches/transforms-3/82360dce7c90d10221f06f4ddfa5bc67/transformed/lifecycle-livedata-core-ktx-2.5.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/8d444fd9988175cecd13055f04813b91/transformed/lifecycle-livedata-core-2.5.0-api.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/androidx.lifecycle/lifecycle-common/2.5.0/1fdb7349701e9cf2f0a69fc10642b6fef6bb3e12/lifecycle-common-2.5.0.jar, /Users/weichou/.gradle/caches/transforms-3/afa292a87f73b3eaac7c83ceefd92574/transformed/interpolator-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/a7341bbf557f0eee7488093003db0f01/transformed/documentfile-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/46ecd33fc8b0aff76d637a8fc3226518/transformed/localbroadcastmanager-1.0.0-api.jar, /Users/weichou/.gradle/caches/transforms-3/2e9cbd427ac36e8b9e40572d70105d5c/transformed/print-1.0.0-api.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/androidx.annotation/annotation/1.3.0/21f49f5f9b85fc49de712539f79123119740595/annotation-1.3.0.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.7.20/2a729aa8763306368e665e2b747abd1dfd29b9d5/kotlin-stdlib-jdk7-1.7.20.jar, /Users/weichou/.gradle/caches/transforms-3/f45fdb3a6f32f3117a02913a5475531b/transformed/annotation-experimental-1.3.0-api.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.7.20/726594ea9ba2beb2ee113647fefa9a10f9fabe52/kotlin-stdlib-1.7.20.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.7.20/e15351bdaf9fa06f009be5da7a202e4184f00ae3/kotlin-stdlib-common-1.7.20.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar, /Users/weichou/.gradle/caches/transforms-3/b61a19ce0ffb6b0eab4a6ede97932e2f/transformed/constraintlayout-2.1.4-api.jar, /Users/weichou/.gradle/caches/transforms-3/632e82547c61849d331ad6e31a1fb88c/transformed/annoid-af2b53cfce-api.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/com.github.dedge-space/scala-lang/253dc64cf9/51c97f073e45e5183af054e4596869d035f47b2d/scala-lang-253dc64cf9.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.scala-lang/scala-compiler/2.11.12/a1b5e58fd80cb1edc1413e904a346bfdb3a88333/scala-compiler-2.11.12.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.scala-lang/scala-reflect/2.11.12/2bb23c13c527566d9828107ca4108be2a2c06f01/scala-reflect-2.11.12.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.scala-lang.modules/scala-xml_2.11/1.0.5/77ac9be4033768cf03cc04fbd1fc5e5711de2459/scala-xml_2.11-1.0.5.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.scala-lang.modules/scala-parser-combinators_2.11/1.0.4/7369d653bcfa95d321994660477a4d7e81d7f490/scala-parser-combinators_2.11-1.0.4.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/org.scala-lang/scala-library/2.12.17/4a4dee1ebb59ed1dbce014223c7c42612e4cddde/scala-library-2.12.17.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/com.github.dedge-space/annoguard/v1.0.5-beta/d9f31382b1d2d4bbf8e34de4b7ef6a547277cfdb/annoguard-v1.0.5-beta.jar, /Users/weichou/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.0/c4ba5371a29ac9b2ad6129b1d39ea38750043eff/gson-2.8.0.jar, /Users/weichou/git/bdo.cash/demo-material-3/app/build/tmp/kotlin-classes/githubDebug] 933 | //LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]variant.getCompileClasspath(classpathKey):${compileClasspath.files}" 934 | 935 | // Configuration 'githubDebugCompileClasspath' was resolved during configuration time. 已经挪到上边了。 936 | /*scalaTask.configure { scala -> // ... 937 | scala.classpath += project.files(variant.getCompileClasspath(classpathKey).files.find { it.path.endsWith("${variant.name}/R.jar") }) 938 | }*/ 939 | // scalaTask.configure { scala -> // ... 940 | // scala.classpath += compileClasspath 941 | // } 942 | // 该方法没有接口,无法调用。 943 | //final prevRegistered = variant.getGeneratedBytecode() 944 | // 根据源码分析,只有没注册过的,才需进行注册(不过这里已经是构建的最后了,没有后续编译任务依赖这个 compileClasspath,用不上了)。 945 | // final clzPathKey = variant.registerPreJavacGeneratedBytecode(scalaOuts) 946 | // kotlinTask.configure { kt -> // `kt.classpath`用`kt.libraries`替代了。 947 | //final files = kt.libraries as ConfigurableFileCollection 948 | //files.from(variant.getGeneratedBytecode() - prevRegistered) //variant.getCompileClasspath(clzPathKey) - files) 949 | // kt.libraries.from(scalaOuts) // 根据上文和源码,直接这样用即可。 950 | 951 | /*if (!kt.incremental) { 952 | LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]variant:${variant.name}, kt.incremental:${kt.incremental}, change to true." 953 | //kt.incremental = true // 用不到下面的,该值也就不用设置了。 954 | }*/ 955 | // 报错:property 'classpathSnapshotProperties.useClasspathSnapshot' cannot be changed any further. 956 | // 根据源码,发现`kt.classpathSnapshotProperties`的任何值都是不能改变的:`task.classpathSnapshotProperties.classpathSnapshot.from(xxx).disallowChanges()`。 957 | // TODO: 要改变它的值,需要在 gradle.properties 中增加一行: 958 | // `kotlin.incremental.useClasspathSnapshot=true` 959 | // 实测通过,写在 local.properties 也可以。 960 | // 不过,实测不适用于 scala-kotlin 交叉编译。https://blog.jetbrains.com/zh-hans/kotlin/2022/07/a-new-approach-to-incremental-compilation-in-kotlin/ 961 | //kt.classpathSnapshotProperties.classpathSnapshot.from(scalaOuts) 962 | /*if (!kt.classpathSnapshotProperties.useClasspathSnapshot.get()) { 963 | LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]variant:${variant.name}, useClasspathSnapshot:${kt.classpathSnapshotProperties.useClasspathSnapshot.get()}, change to true is disallow." 964 | //kt.classpathSnapshotProperties.useClasspathSnapshot.set(true) 965 | }*/ 966 | // } 967 | 968 | // TODO: 综上,简写如下: 969 | LOG.info "$NAME_PLUGIN ---> [wireScalaTasks]scalaCodeReferToKt:${scalroid.scalaCodeReferToKt.get()}, ktCodeReferToScala:${scalroid.ktCodeReferToScala.get()}" 970 | checkArgsProbablyWarning(scalroid) 971 | 972 | // final classpathKey = variant.registerPreJavacGeneratedBytecode(scalaOuts) 973 | final classpathKey = variant.registerPreJavacGeneratedBytecode(scalaClasses) 974 | if (scalroid.scalaCodeReferToKt.get()) { 975 | scalaTask.configure { scala -> // ... 976 | scala.classpath += variant.getCompileClasspath(classpathKey) 977 | } 978 | // 抑制警告:- Gradle detected a problem with the following location: '/Users/weichou/git/bdo.cash/demo-material-3/app/build/tmp/scala-classes/githubDebug'. 979 | // Reason: Task ':app:mergeGithubDebugJavaResource' uses this output of task ':app:compileGithubDebugScala' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. Please refer to https://docs.gradle.org/7.4/userguide/validation_problems.html#implicit_dependency for more details about this problem. 980 | final mergeJavaRes = project.tasks.findByName(genMergeJavaResourceTaskName(variant.name)) 981 | // if (mergeJavaRes) mergeJavaRes.dependsOn scalaTask 982 | if (mergeJavaRes) mergeJavaRes.dependsOn deduplicate 983 | } else { 984 | scalaOuts.builtBy(scalaTask) 985 | scalaClasses.builtBy(deduplicate) 986 | } 987 | if (scalroid.ktCodeReferToScala.get()) { 988 | kotlinTask.configure { kt -> // ... 989 | // kt.libraries.from(scalaOuts) 990 | kt.libraries.from(scalaClasses) 991 | } 992 | } 993 | } 994 | 995 | ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// ////////// 996 | private void configureScaladoc(Project project, mainSourceSet, ScalaCompile mainScalaCompile) { 997 | project.tasks.withType(ScalaDoc).configureEach { ScalaDoc scalaDoc -> 998 | scalaDoc.conventionMapping.map("classpath", (new Callable() { 999 | @Override 1000 | FileCollection call() { 1001 | LOG.info "$NAME_PLUGIN ---> [configureScaladoc] >>>" 1002 | return project.files().from(mainScalaCompile.outputs, mainScalaCompile.classpath) 1003 | } 1004 | })) 1005 | scalaDoc.setSource(mainSourceSet.extensions.getByType(ScalaSourceDirectorySet)) 1006 | scalaDoc.compilationOutputs.from(mainScalaCompile.outputs) 1007 | } 1008 | project.tasks.register(ScalaPlugin.SCALA_DOC_TASK_NAME, ScalaDoc) { ScalaDoc scalaDoc -> 1009 | scalaDoc.setDescription("Generates Scaladoc for the main source code.") 1010 | scalaDoc.setGroup(JavaBasePlugin.DOCUMENTATION_GROUP) 1011 | } 1012 | } 1013 | 1014 | private String genJavaCompileTaskName(String srcSetNameMatchVariant) { 1015 | return "compile${srcSetNameMatchVariant.capitalize()}JavaWithJavac" 1016 | } 1017 | 1018 | private String genKotlinCompileTaskName(String srcSetNameMatchVariant) { 1019 | return "compile${srcSetNameMatchVariant.capitalize()}Kotlin" 1020 | } 1021 | 1022 | private String genScalaCompileTaskName(String srcSetNameMatchVariant) { 1023 | return "compileScala${srcSetNameMatchVariant.capitalize()}CrossScope" 1024 | } 1025 | 1026 | private String genDeduplicateClassesTaskName(String srcSetNameMatchVariant) { 1027 | return "compile${srcSetNameMatchVariant.capitalize()}Scala" 1028 | } 1029 | 1030 | private String genMergeJavaResourceTaskName(String srcSetNameMatchVariant) { 1031 | return "merge${srcSetNameMatchVariant.capitalize()}JavaResource" 1032 | } 1033 | 1034 | private String genBuildConfigTaskName(String srcSetNameMatchVariant) { 1035 | return "generate${srcSetNameMatchVariant.capitalize()}BuildConfig" 1036 | } 1037 | 1038 | private String genProcessResourcesTaskName(String srcSetNameMatchVariant) { 1039 | return "process${srcSetNameMatchVariant.capitalize()}Resources" 1040 | } 1041 | 1042 | private String genRFileTaskName(String srcSetNameMatchVariant) { 1043 | return "generate${srcSetNameMatchVariant.capitalize()}RFile" 1044 | } 1045 | 1046 | private String genDataBindingGenBaseTaskName(String srcSetNameMatchVariant) { 1047 | return "dataBindingGenBaseClasses${srcSetNameMatchVariant.capitalize()}" 1048 | } 1049 | 1050 | private String parseSimpleVariantNameForTest(String name) { 1051 | //assert maybeIsTest(name) 1052 | if (/*name == main || */ name == androidTest || name == test) { 1053 | return name 1054 | } else if (name.endsWith(androidTest.capitalize())) { 1055 | return name.substring(0, name.length() - androidTest.length()) 1056 | } else if (name.endsWith(unitTest.capitalize())) { 1057 | return name.substring(0, name.length() - unitTest.length()) 1058 | } else { 1059 | assert name.endsWith(test.capitalize()) 1060 | return name.substring(0, name.length() - test.length()) 1061 | } 1062 | } 1063 | 1064 | private boolean maybeIsTest(String name) { 1065 | final isTest = name == androidTest || name == test || name == unitTest || name.endsWith(androidTest.capitalize()) || name.endsWith(test.capitalize()) || name.endsWith(unitTest.capitalize()) 1066 | assert isTest || !(name.contains(androidTest) || name.contains(androidTest.capitalize()) || name.contains(test) || name.contains(test.capitalize())) 1067 | return isTest 1068 | } 1069 | 1070 | private String adjustSourceSetNameToMatchVariant(String sourceSetName, Set variantsNames) { 1071 | String srcSetNameMatchVariant = sourceSetName 1072 | if (srcSetNameMatchVariant != main && srcSetNameMatchVariant != test) { 1073 | if (!variantsNames || !variantsNames.contains(srcSetNameMatchVariant)) { 1074 | if (srcSetNameMatchVariant.startsWith(androidTest)) { 1075 | if (srcSetNameMatchVariant != androidTest) srcSetNameMatchVariant = srcSetNameMatchVariant.substring(androidTest.length()).uncapitalize() + androidTest.capitalize() 1076 | } else if (srcSetNameMatchVariant.startsWith(test)) { // 注意`unitTest`比较特殊 1077 | if (srcSetNameMatchVariant != test) srcSetNameMatchVariant = srcSetNameMatchVariant.substring(test.length()).uncapitalize() + unitTest.capitalize() 1078 | } else if (srcSetNameMatchVariant.contains(androidTest.capitalize()) || srcSetNameMatchVariant.contains(test.capitalize()) || srcSetNameMatchVariant.contains(androidTest) || srcSetNameMatchVariant.contains(test)) { 1079 | // 不在开头,那就在中间或结尾,即:`androidTest`或`test`的首字母大写。 1080 | //LOG.info "$NAME_PLUGIN ---> exception:${srcSetNameMatchVariant}" 1081 | throw new ProjectConfigurationException("sourceSet.name(${sourceSet.name}) not contains in `variants.names` and not within the expected range, please check.", new Throwable()) 1082 | } 1083 | } 1084 | if (variantsNames && !variantsNames.contains(srcSetNameMatchVariant)) { 1085 | LOG.info "$NAME_PLUGIN ||| return (srcSetNameMatchVariant:$srcSetNameMatchVariant)" 1086 | srcSetNameMatchVariant = null 1087 | } 1088 | } 1089 | LOG.info "$NAME_PLUGIN ---> srcSetNameMatchVariant:${srcSetNameMatchVariant}" 1090 | return srcSetNameMatchVariant 1091 | } 1092 | 1093 | private void printConfiguration(Project project, configName) { 1094 | //project.configurations.all { Configuration config -> 1095 | // LOG.info "$NAME_PLUGIN ---> configurations.name: ${config.name} -------- √√√" 1096 | // config.getDependencies().each { Dependency dep -> // 1097 | // LOG.info " configurations.dependencies: ${dep.group}:${dep.name}:${dep.version}" 1098 | // } 1099 | // LOG.info '' 1100 | //} 1101 | project.configurations.named(configName).configure { Configuration config -> 1102 | LOG.warn "$NAME_PLUGIN ---> configurations.name:${config.name} -------- √" 1103 | config.dependencies.each { Dependency dep -> // 1104 | LOG.warn " `${configName} ${dep.group}:${dep.name}:${dep.version}`" 1105 | LOG.warn " ${' ' * configName.length()} > ${dep} / ${dep.class}" 1106 | } 1107 | LOG.warn '' 1108 | } 1109 | } 1110 | 1111 | private void printClasspathOnTaskDoFirst(Project project, Task task, Configuration classpathConfig, @Nullable String src$vaName, @Nullable String projNameOnly) { 1112 | task.doFirst { 1113 | try { 1114 | if (!projNameOnly || project.name == projNameOnly) { 1115 | LOG.warn "$NAME_PLUGIN ---> [printClasspath]project:${project.name}, variant:${src$vaName}" 1116 | LOG.warn "$NAME_PLUGIN ---> [printClasspath]project.parent:${project.parent.name}, project.root:${project.rootProject.name}, project.depth:${project.depth}" 1117 | classpathConfig.each { 1118 | LOG.warn "${it.path}${it.path.endsWith('.aar') ? ' ×' : ''}" 1119 | } 1120 | LOG.warn '' 1121 | } 1122 | } catch (e) { 1123 | LOG.warn "$NAME_PLUGIN ---> [printClasspath]$e" 1124 | } 1125 | } 1126 | } 1127 | /*private List findMainSubProject(Project project) { 1128 | // 无法拿到其它 subproject 的配置信息,时序问题:apply `com.android.application`的 project 还未开始配置。 1129 | LOG.info "$NAME_PLUGIN ---> [findMainSubProject]${project.rootProject} | name:${project.rootProject.name}, path:${project.rootProject.path}, version:${project.rootProject.version}" 1130 | final subPrs = [] 1131 | project.rootProject.subprojects { proj -> 1132 | println proj 1133 | if (proj.plugins.findPlugin(ID_ANDROID_APP)) { 1134 | println "findPlugin $ID_ANDROID_APP" 1135 | subPrs.add(proj) 1136 | } 1137 | } 1138 | println subPrs 1139 | return subPrs 1140 | }*/ 1141 | } 1142 | --------------------------------------------------------------------------------