Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

此插件令下游Transform崩溃 “Unexpected scopes found in folder” #44

Open
fish47 opened this issue Mar 7, 2021 · 0 comments
Open

Comments

@fish47
Copy link

fish47 commented Mar 7, 2021

直接原因

尝试创建新的SubStream

abstract class SimpleClassCreatorTransform extends Transform {

    String prepareToCreateClass(TransformInvocation transformInvocation) {
        ...
        return transformInvocation.outputProvider.getContentLocation("main",
                getOutputTypes(), getScopes(),
                Format.DIRECTORY)
    }
}

Scope集合作为TransformOutputProvider.getContentLocation()有问题

class SplitComponentTransform extends SimpleClassCreatorTransform {

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }
}

详见build目录下的__content__.json,基本没有混合ScopeSubStream

[
    {
        "scopes": [
            "EXTERNAL_LIBRARIES"
        ],
        "format": "JAR",
        "types": [ "DEX_ARCHIVE" ],
        "name": "...",
        "index": 0,
        "present": true
    },
    {
        "scopes": [ "SUB_PROJECTS" ],
        "format": "JAR",
        "types": [ "DEX_ARCHIVE" ],
        "name": "...",
        "index": 352,
        "present": true
    },
    {
        "scopes": [ "PROJECT" ],
        "format": "DIRECTORY",
        "types": [ "DEX_ARCHIVE" ],
        "name": "...",
        "index": 354,
        "present": true
    },
]

根本原因

  • 每个Transform都会"承上启下"地对应一个IntermediateStream
  • 每个IntermediateStream对应多个SubStream
  • 上下游的SubStreamScope匹配,大部分情况下游是上游的超集,但如果是部分包含则抛异常

影响范围

所有使用TransformScope不包含EXTERNAL_LIBRARIES的插件。

解决方法

  • 修正TransformOutputProvider.getContentLocation()参数
    建议改为ImmutableSet.of(QualifiedContent.Scope.PROJECT)

  • 改用代码生成实现此流程
    SplitComponentTransform的业务与R.java生成类似,但后者没有侵入到字节码编辑流程。

重现代码

新建一个Android工程,然后把下面代码粘贴到主工程的build.gradle最后。

import com.android.annotations.NonNull
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.Status
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.build.api.transform.Transform
import com.android.build.api.transform.QualifiedContent
import com.google.common.collect.ImmutableSet
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes


abstract class TransformBase extends Transform {

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return ImmutableSet.of(QualifiedContent.DefaultContentType.CLASSES)
    }

    private static File getOutputFile(TransformInvocation invocation, QualifiedContent content, Format format) {
        def provider = invocation.outputProvider
        return provider.getContentLocation(content.name, content.contentTypes, content.scopes, format)
    }

    private static void transformJar(TransformInvocation invocation, JarInput jar) {
        if (jar.status == Status.REMOVED) {
            return
        }

        def outFile = getOutputFile(invocation, jar, Format.JAR)
        FileUtils.copyFile(jar.file, outFile)
    }

    private static void transformDirectory(TransformInvocation invocation, DirectoryInput dir) {
        if (!dir.file.exists()) {
            return
        }

        def statusMap = dir.getChangedFiles()
        def outDir = getOutputFile(invocation, dir, Format.DIRECTORY)
        FileUtils.copyDirectory(dir.file, outDir, { statusMap.getOrDefault(it, Status.NOTCHANGED) != Status.REMOVED })
    }

    @Override
    void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        transformInvocation.inputs.each { TransformInput input ->
            input.jarInputs.each { transformJar(transformInvocation, it) }
            input.directoryInputs.each { transformDirectory(transformInvocation, it) }
        }
    }
}

class CopyTransform extends TransformBase {

    @Override
    String getName() {
        return "copy"
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return ImmutableSet.of(
                QualifiedContent.Scope.PROJECT,
                QualifiedContent.Scope.SUB_PROJECTS
        )
    }
}

class GenClassTransform extends TransformBase {

    private static final String FOO_CLASS_NAME = "Foo"

    private static final String FOO_CLASS_PACKAGE = "com.fish47.foo"

    @Override
    String getName() {
        return "genClass"
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    private static getGenerateClassFile(File dir) {
        def subDir = FOO_CLASS_PACKAGE.replace(".", File.separator)
        def subPath = String.format("%s%c%s.class", subDir, File.separatorChar, FOO_CLASS_NAME)
        return new File(dir, subPath)
    }

    private static byte[] getGenerateClassBytes() {
        def fullName = String.format("%s/%s", FOO_CLASS_PACKAGE.replace(".", "/"), FOO_CLASS_NAME)
        def clzAcc = Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER
        def cw = new ClassWriter(0)
        cw.visit(49, clzAcc, fullName, null, "java/lang/Object", null)

        def mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null)
        mv.visitVarInsn(Opcodes.ALOAD, 0)
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)
        mv.visitInsn(Opcodes.RETURN)
        mv.visitMaxs(1, 1)
        mv.visitEnd()

        cw.visitEnd()
        return cw.toByteArray()
    }

    private static generateFooClass(TransformInvocation invocation) {
        def provider = invocation.outputProvider
        def types = ImmutableSet.of(QualifiedContent.DefaultContentType.CLASSES)
        def scopes = TransformManager.SCOPE_FULL_PROJECT
        def outDir = provider.getContentLocation("main", types, scopes, Format.DIRECTORY)

        def classFile = getGenerateClassFile(outDir)
        FileUtils.writeByteArrayToFile(classFile, getGenerateClassBytes())
    }

    @Override
    void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        generateFooClass(transformInvocation)
    }
}

def androidExtension = project.extensions.findByName("android")
if (androidExtension instanceof BaseExtension) {
    // ok
//    androidExtension.registerTransform(new CopyTransform())
//    androidExtension.registerTransform(new GenClassTransform())

    // crash
    androidExtension.registerTransform(new GenClassTransform())
    androidExtension.registerTransform(new CopyTransform())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant