[JS IR] Invalidate all klib dependencies after removing it
Without the invalidation, broken JS code (with broken cross-module references) may appear. ^KT-54911 Fixed
This commit is contained in:
committed by
Space Team
parent
ca19d71a00
commit
693258ae91
@@ -665,18 +665,24 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
|
||||
|
||||
var libIndex = 0
|
||||
for ((libFile, srcFiles) in cacheUpdater.getDirtyFileLastStats()) {
|
||||
val singleState = srcFiles.values.firstOrNull()?.singleOrNull()?.let { singleState ->
|
||||
singleState.takeIf { srcFiles.values.all { it.singleOrNull() == singleState } }
|
||||
}
|
||||
|
||||
val (msg, showFiles) = when {
|
||||
srcFiles.values.all { it.contains(DirtyFileState.ADDED_FILE) } -> "fully rebuilt" to false
|
||||
else -> "partially rebuilt" to true
|
||||
singleState == DirtyFileState.NON_MODIFIED_IR -> continue
|
||||
singleState == DirtyFileState.REMOVED_FILE -> "removed" to emptyMap()
|
||||
singleState == DirtyFileState.ADDED_FILE -> "built clean" to emptyMap()
|
||||
srcFiles.values.any { it.singleOrNull() == DirtyFileState.NON_MODIFIED_IR } -> "partially rebuilt" to srcFiles
|
||||
else -> "fully rebuilt" to srcFiles
|
||||
}
|
||||
messageCollector.report(INFO, "${++libIndex}) module [${File(libFile.path).name}] was $msg")
|
||||
if (showFiles) {
|
||||
var fileIndex = 0
|
||||
for ((srcFile, stat) in srcFiles) {
|
||||
val statStr = stat.joinToString { it.str }
|
||||
// Use index, because MessageCollector ignores already reported messages
|
||||
messageCollector.report(INFO, " $libIndex.${++fileIndex}) file [${File(srcFile.path).name}]: ($statStr)")
|
||||
}
|
||||
var fileIndex = 0
|
||||
for ((srcFile, stat) in showFiles) {
|
||||
val filteredStats = stat.filter { it != DirtyFileState.NON_MODIFIED_IR }
|
||||
val statStr = filteredStats.takeIf { it.isNotEmpty() }?.joinToString { it.str } ?: continue
|
||||
// Use index, because MessageCollector ignores already reported messages
|
||||
messageCollector.report(INFO, " $libIndex.${++fileIndex}) file [${File(srcFile.path).name}]: ($statStr)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.jetbrains.kotlin.library.KotlinLibrary
|
||||
import org.jetbrains.kotlin.library.resolver.TopologicalLibraryOrder
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.util.EnumSet
|
||||
|
||||
fun interface JsIrCompilerICInterface {
|
||||
@@ -38,6 +39,7 @@ fun interface JsIrCompilerICInterfaceFactory {
|
||||
enum class DirtyFileState(val str: String) {
|
||||
ADDED_FILE("added file"),
|
||||
MODIFIED_IR("modified ir"),
|
||||
NON_MODIFIED_IR("non modified ir"),
|
||||
UPDATED_EXPORTS("updated exports"),
|
||||
UPDATED_IMPORTS("updated imports"),
|
||||
REMOVED_INVERSE_DEPENDS("removed inverse depends"),
|
||||
@@ -106,14 +108,25 @@ class CacheUpdater(
|
||||
private val incrementalCaches = libraryDependencies.keys.associate { lib ->
|
||||
val libFile = KotlinLibraryFile(lib)
|
||||
val file = File(libFile.path)
|
||||
libFile to IncrementalCache(lib, File(cacheRootDir, "${file.name}.${file.absolutePath.stringHashForIC()}"))
|
||||
val libraryCacheDir = File(cacheRootDir, "${file.name}.${file.absolutePath.stringHashForIC()}")
|
||||
libFile to IncrementalCache(KotlinLoadedLibraryHeader(lib), libraryCacheDir)
|
||||
}
|
||||
|
||||
private val removedIncrementalCaches = buildList {
|
||||
if (cacheRootDir.isDirectory) {
|
||||
val availableCaches = incrementalCaches.values.mapTo(HashSet(incrementalCaches.size)) { it.cacheDir }
|
||||
val allDirs = Files.walk(cacheRootDir.toPath(), 1).map { it.toFile() }
|
||||
allDirs.filter { it != cacheRootDir && it !in availableCaches }.forEach { removedCacheDir ->
|
||||
add(IncrementalCache(KotlinRemovedLibraryHeader(removedCacheDir), removedCacheDir))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLibIncrementalCache(libFile: KotlinLibraryFile) =
|
||||
incrementalCaches[libFile] ?: notFoundIcError("incremental cache", libFile)
|
||||
|
||||
private fun addFilesWithRemovedDependencies(
|
||||
modifiedFiles: KotlinSourceFileMap<KotlinSourceFileMetadata>,
|
||||
modifiedFiles: KotlinSourceFileMutableMap<KotlinSourceFileMetadata>,
|
||||
removedFiles: KotlinSourceFileMap<KotlinSourceFileMetadata>
|
||||
): KotlinSourceFileMap<KotlinSourceFileMetadata> {
|
||||
val extraModifiedLibFiles = KotlinSourceFileMutableMap<KotlinSourceFileMetadata>()
|
||||
@@ -145,36 +158,42 @@ class CacheUpdater(
|
||||
addDependenciesToExtraModifiedFiles(removedFileMetadata.inverseDependencies, DirtyFileState.REMOVED_DIRECT_DEPENDS)
|
||||
}
|
||||
|
||||
if (extraModifiedLibFiles.isNotEmpty()) {
|
||||
extraModifiedLibFiles.copyFilesFrom(modifiedFiles)
|
||||
return extraModifiedLibFiles
|
||||
}
|
||||
modifiedFiles.copyFilesFrom(extraModifiedLibFiles)
|
||||
return modifiedFiles
|
||||
}
|
||||
|
||||
fun loadModifiedFiles(): KotlinSourceFileMap<KotlinSourceFileMetadata> {
|
||||
val removedFilesMetadata = hashMapOf<KotlinLibraryFile, Map<KotlinSourceFile, KotlinSourceFileMetadata>>()
|
||||
|
||||
val modifiedFiles = KotlinSourceFileMap(incrementalCaches.entries.associate { (lib, cache) ->
|
||||
val (dirtyFiles, removedFiles, newFiles) = cache.collectModifiedFiles()
|
||||
fun collectDirtyFiles(lib: KotlinLibraryFile, cache: IncrementalCache): MutableMap<KotlinSourceFile, KotlinSourceFileMetadata> {
|
||||
val (addedFiles, removedFiles, modifiedFiles, nonModifiedFiles) = cache.collectModifiedFiles()
|
||||
|
||||
val fileStats by lazy(LazyThreadSafetyMode.NONE) { dirtyFileStats.getOrPutFiles(lib) }
|
||||
newFiles.forEach { fileStats.addDirtFileStat(it, DirtyFileState.ADDED_FILE) }
|
||||
addedFiles.forEach { fileStats.addDirtFileStat(it, DirtyFileState.ADDED_FILE) }
|
||||
removedFiles.forEach { fileStats.addDirtFileStat(it.key, DirtyFileState.REMOVED_FILE) }
|
||||
dirtyFiles.forEach {
|
||||
if (it.key !in newFiles) {
|
||||
fileStats.addDirtFileStat(it.key, DirtyFileState.MODIFIED_IR)
|
||||
}
|
||||
}
|
||||
modifiedFiles.forEach { fileStats.addDirtFileStat(it.key, DirtyFileState.MODIFIED_IR) }
|
||||
nonModifiedFiles.forEach { fileStats.addDirtFileStat(it, DirtyFileState.NON_MODIFIED_IR) }
|
||||
|
||||
if (removedFiles.isNotEmpty()) {
|
||||
removedFilesMetadata[lib] = removedFiles
|
||||
}
|
||||
|
||||
lib to dirtyFiles
|
||||
})
|
||||
return addedFiles.associateWithTo(modifiedFiles.toMutableMap()) { KotlinSourceFileMetadataNotExist }
|
||||
}
|
||||
|
||||
return addFilesWithRemovedDependencies(modifiedFiles, KotlinSourceFileMap(removedFilesMetadata))
|
||||
for (cache in removedIncrementalCaches) {
|
||||
val libFile = cache.libraryFileFromHeader ?: notFoundIcError("removed library name; cache dir: ${cache.cacheDir}")
|
||||
val dirtyFiles = collectDirtyFiles(libFile, cache)
|
||||
if (dirtyFiles.isNotEmpty()) {
|
||||
icError("unexpected dirty file", libFile, dirtyFiles.keys.first())
|
||||
}
|
||||
}
|
||||
|
||||
val dirtyFiles = incrementalCaches.entries.associateTo(hashMapOf()) { (lib, cache) ->
|
||||
lib to collectDirtyFiles(lib, cache)
|
||||
}
|
||||
|
||||
return addFilesWithRemovedDependencies(KotlinSourceFileMutableMap(dirtyFiles), KotlinSourceFileMap(removedFilesMetadata))
|
||||
}
|
||||
|
||||
fun collectExportedSymbolsForDirtyFiles(
|
||||
@@ -493,10 +512,15 @@ class CacheUpdater(
|
||||
}
|
||||
}
|
||||
|
||||
fun buildCacheArtifacts(
|
||||
fun buildAndCommitCacheArtifacts(
|
||||
jsIrLinker: JsIrLinker,
|
||||
loadedFragments: Map<KotlinLibraryFile, IrModuleFragment>
|
||||
): Map<KotlinLibraryFile, IncrementalCacheArtifact> {
|
||||
removedIncrementalCaches.forEach {
|
||||
if (!it.cacheDir.deleteRecursively()) {
|
||||
icError("can not delete cache directory ${it.cacheDir.absolutePath}")
|
||||
}
|
||||
}
|
||||
return libraryDependencies.keys.associate { library ->
|
||||
val libFile = KotlinLibraryFile(library)
|
||||
val incrementalCache = getLibIncrementalCache(libFile)
|
||||
@@ -616,7 +640,7 @@ class CacheUpdater(
|
||||
updater.updateStdlibIntrinsicDependencies(loadedIr.linker, mainModuleFragment, loadedIr.loadedFragments, dirtyFiles)
|
||||
|
||||
stopwatch.startNext("Incremental cache - building artifacts")
|
||||
val incrementalCachesArtifacts = updater.buildCacheArtifacts(loadedIr.linker, loadedIr.loadedFragments)
|
||||
val incrementalCachesArtifacts = updater.buildAndCommitCacheArtifacts(loadedIr.linker, loadedIr.loadedFragments)
|
||||
|
||||
stopwatch.stop()
|
||||
return IrForDirtyFilesAndCompiler(incrementalCachesArtifacts, loadedIr.loadedFragments, dirtyFiles, compilerForIC)
|
||||
@@ -676,7 +700,7 @@ fun rebuildCacheForDirtyFiles(
|
||||
}
|
||||
|
||||
val libFile = KotlinLibraryFile(library)
|
||||
val dirtySrcFiles = dirtyFiles?.map { KotlinSourceFile(it) } ?: KotlinLibraryHeader(library).sourceFiles
|
||||
val dirtySrcFiles = dirtyFiles?.map { KotlinSourceFile(it) } ?: KotlinLoadedLibraryHeader(library).sourceFileFingerprints.keys
|
||||
val modifiedFiles = mapOf(libFile to dirtySrcFiles.associateWith { emptyMetadata })
|
||||
|
||||
val jsIrLoader = JsIrLinkerLoader(configuration, dependencyGraph, irFactory)
|
||||
|
||||
@@ -48,10 +48,6 @@ internal fun notFoundIcError(what: String, libFile: KotlinLibraryFile? = null, s
|
||||
icError("can not find $what", libFile, srcFile)
|
||||
}
|
||||
|
||||
internal inline fun <E> buildListUntil(to: Int, builderAction: MutableList<E>.(Int) -> Unit): List<E> {
|
||||
return buildList(to) { repeat(to) { builderAction(it) } }
|
||||
}
|
||||
|
||||
internal inline fun <E> buildSetUntil(to: Int, builderAction: MutableSet<E>.(Int) -> Unit): Set<E> {
|
||||
return HashSet<E>(to).apply { repeat(to) { builderAction(it) } }
|
||||
}
|
||||
|
||||
+57
-75
@@ -6,17 +6,13 @@
|
||||
package org.jetbrains.kotlin.ir.backend.js.ic
|
||||
|
||||
import org.jetbrains.kotlin.backend.common.serialization.IdSignatureDeserializer
|
||||
import org.jetbrains.kotlin.ir.backend.js.jsOutputName
|
||||
import org.jetbrains.kotlin.ir.util.IdSignature
|
||||
import org.jetbrains.kotlin.library.KotlinLibrary
|
||||
import org.jetbrains.kotlin.library.impl.javaFile
|
||||
import org.jetbrains.kotlin.protobuf.CodedInputStream
|
||||
import org.jetbrains.kotlin.protobuf.CodedOutputStream
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.ifTrue
|
||||
import java.io.File
|
||||
|
||||
|
||||
internal class IncrementalCache(private val library: KotlinLibrary, val cacheDir: File) {
|
||||
internal class IncrementalCache(private val library: KotlinLibraryHeader, val cacheDir: File) {
|
||||
companion object {
|
||||
private const val CACHE_HEADER = "ic.header.bin"
|
||||
|
||||
@@ -25,76 +21,71 @@ internal class IncrementalCache(private val library: KotlinLibrary, val cacheDir
|
||||
private const val METADATA_TMP_SUFFIX = "metadata.tmp.bin"
|
||||
}
|
||||
|
||||
private val signatureToIndexMappingFromMetadata = hashMapOf<KotlinSourceFile, MutableMap<IdSignature, Int>>()
|
||||
|
||||
private val libraryFile = KotlinLibraryFile(library)
|
||||
|
||||
private val cacheHeaderFile = File(cacheDir, CACHE_HEADER)
|
||||
|
||||
private var newCacheHeader: CacheHeader? = null
|
||||
private var cacheHeaderShouldBeUpdated = false
|
||||
|
||||
private var removedSrcFiles: Collection<KotlinSourceFile> = emptyList()
|
||||
|
||||
private val kotlinLibraryHeader: KotlinLibraryHeader by lazy { KotlinLibraryHeader(library) }
|
||||
private val kotlinLibrarySourceFileMetadata = hashMapOf<KotlinSourceFile, KotlinSourceFileMetadata>()
|
||||
|
||||
private val signatureToIndexMappingFromMetadata = hashMapOf<KotlinSourceFile, MutableMap<IdSignature, Int>>()
|
||||
|
||||
private val cacheHeaderFromDisk by lazy {
|
||||
cacheHeaderFile.useCodedInputIfExists {
|
||||
CacheHeader.fromProtoStream(this, library.libraryFingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
val libraryFileFromHeader by lazy { cacheHeaderFromDisk?.libraryFile }
|
||||
|
||||
private class CacheHeader(
|
||||
val libraryFile: KotlinLibraryFile,
|
||||
private val libraryFingerprint: ICHash?,
|
||||
val sourceFileFingerprints: Map<KotlinSourceFile, ICHash>?
|
||||
) {
|
||||
constructor(library: KotlinLibraryHeader) : this(library.libraryFile, library.libraryFingerprint, library.sourceFileFingerprints)
|
||||
|
||||
private class CacheHeader(val klibFileHash: ICHash, val klibFile: KotlinLibraryFile, val fingerprints: Map<KotlinSourceFile, ICHash>?) {
|
||||
fun toProtoStream(out: CodedOutputStream) {
|
||||
klibFile.toProtoStream(out)
|
||||
klibFileHash.toProtoStream(out)
|
||||
libraryFile.toProtoStream(out)
|
||||
|
||||
if (fingerprints == null) {
|
||||
notFoundIcError("fingerprints", klibFile)
|
||||
}
|
||||
out.writeInt32NoTag(fingerprints.size)
|
||||
for ((srcFile, fingerprint) in fingerprints) {
|
||||
srcFile.toProtoStream(out)
|
||||
fingerprint.toProtoStream(out)
|
||||
}
|
||||
libraryFingerprint?.toProtoStream(out) ?: notFoundIcError("library fingerprint", libraryFile)
|
||||
|
||||
sourceFileFingerprints?.let { fingerprints ->
|
||||
out.writeInt32NoTag(fingerprints.size)
|
||||
for ((srcFile, fingerprint) in fingerprints) {
|
||||
srcFile.toProtoStream(out)
|
||||
fingerprint.toProtoStream(out)
|
||||
}
|
||||
} ?: notFoundIcError("source file fingerprints", libraryFile)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromProtoStream(input: CodedInputStream, newKlibFileHash: ICHash): CacheHeader {
|
||||
val klibFile = KotlinLibraryFile.fromProtoStream(input)
|
||||
val oldKlibFileHash = ICHash.fromProtoStream(input)
|
||||
fun fromProtoStream(input: CodedInputStream, newLibraryFingerprint: ICHash?): CacheHeader {
|
||||
val libraryFile = KotlinLibraryFile.fromProtoStream(input)
|
||||
val oldLibraryFingerprint = ICHash.fromProtoStream(input)
|
||||
|
||||
val fingerprints = (oldKlibFileHash != newKlibFileHash).ifTrue {
|
||||
val sourceFileFingerprints = (oldLibraryFingerprint != newLibraryFingerprint).ifTrue {
|
||||
buildMapUntil(input.readInt32()) {
|
||||
val file = KotlinSourceFile.fromProtoStream(input)
|
||||
put(file, ICHash.fromProtoStream(input))
|
||||
}
|
||||
}
|
||||
return CacheHeader(oldKlibFileHash, klibFile, fingerprints)
|
||||
return CacheHeader(libraryFile, oldLibraryFingerprint, sourceFileFingerprints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCacheHeader(newKlibFileHash: ICHash): CacheHeader? {
|
||||
return cacheHeaderFile.useCodedInputIfExists {
|
||||
val cacheHeader = CacheHeader.fromProtoStream(this, newKlibFileHash)
|
||||
if (cacheHeader.klibFile != libraryFile) {
|
||||
icError("broken incremental cache header $cacheHeaderFile; expected $libraryFile, got ${cacheHeader.klibFile}")
|
||||
}
|
||||
cacheHeader
|
||||
}
|
||||
}
|
||||
|
||||
private class KotlinSourceFileMetadataFromDisk(
|
||||
override val inverseDependencies: KotlinSourceFileMap<Set<IdSignature>>,
|
||||
override val directDependencies: KotlinSourceFileMap<Map<IdSignature, ICHash>>,
|
||||
) : KotlinSourceFileMetadata()
|
||||
|
||||
private object KotlinSourceFileMetadataNotExist : KotlinSourceFileMetadata() {
|
||||
override val inverseDependencies = KotlinSourceFileMap<Set<IdSignature>>(emptyMap())
|
||||
override val directDependencies = KotlinSourceFileMap<Map<IdSignature, ICHash>>(emptyMap())
|
||||
}
|
||||
|
||||
private val kotlinLibrarySourceFileMetadata = hashMapOf<KotlinSourceFile, KotlinSourceFileMetadata>()
|
||||
|
||||
private fun KotlinSourceFile.getCacheFile(suffix: String) = File(cacheDir, "${File(path).name}.${path.stringHashForIC()}.$suffix")
|
||||
|
||||
fun buildIncrementalCacheArtifact(signatureToIndexMapping: Map<KotlinSourceFile, Map<IdSignature, Int>>): IncrementalCacheArtifact {
|
||||
newCacheHeader?.let { cacheHeader ->
|
||||
cacheHeaderFile.useCodedOutput { cacheHeader.toProtoStream(this) }
|
||||
if (cacheHeaderShouldBeUpdated) {
|
||||
cacheHeaderFile.useCodedOutput { CacheHeader(library).toProtoStream(this) }
|
||||
}
|
||||
|
||||
for (removedFile in removedSrcFiles) {
|
||||
@@ -102,51 +93,42 @@ internal class IncrementalCache(private val library: KotlinLibrary, val cacheDir
|
||||
removedFile.getCacheFile(METADATA_SUFFIX).delete()
|
||||
}
|
||||
|
||||
val fileArtifacts = kotlinLibraryHeader.sourceFiles.map { srcFile ->
|
||||
val fileArtifacts = library.sourceFileFingerprints.keys.map { srcFile ->
|
||||
commitSourceFileMetadata(srcFile.getCacheFile(BINARY_AST_SUFFIX), srcFile, signatureToIndexMapping[srcFile] ?: emptyMap())
|
||||
}
|
||||
return IncrementalCacheArtifact(cacheDir, removedSrcFiles.isNotEmpty(), fileArtifacts, library.jsOutputName)
|
||||
}
|
||||
|
||||
data class ModifiedFiles(
|
||||
val dirtyFiles: Map<KotlinSourceFile, KotlinSourceFileMetadata> = emptyMap(),
|
||||
val addedFiles: List<KotlinSourceFile> = emptyList(),
|
||||
val removedFiles: Map<KotlinSourceFile, KotlinSourceFileMetadata> = emptyMap(),
|
||||
val newFiles: Set<KotlinSourceFile> = emptySet(),
|
||||
val modifiedFiles: Map<KotlinSourceFile, KotlinSourceFileMetadata> = emptyMap(),
|
||||
val nonModifiedFiles: List<KotlinSourceFile> = emptyList()
|
||||
)
|
||||
|
||||
fun collectModifiedFiles(): ModifiedFiles {
|
||||
val klibFileHash = library.libraryFile.javaFile().fileHashForIC()
|
||||
val cachedFingerprints = cacheHeaderFromDisk?.let { it.sourceFileFingerprints ?: return ModifiedFiles() } ?: emptyMap()
|
||||
|
||||
val cacheHeader = loadCacheHeader(klibFileHash)
|
||||
val addedFiles = mutableListOf<KotlinSourceFile>()
|
||||
val modifiedFiles = hashMapOf<KotlinSourceFile, KotlinSourceFileMetadata>()
|
||||
val nonModifiedFiles = mutableListOf<KotlinSourceFile>()
|
||||
|
||||
val cachedFingerprints = cacheHeader?.let { it.fingerprints ?: return ModifiedFiles() } ?: emptyMap()
|
||||
|
||||
val deletedFiles = HashSet(cachedFingerprints.keys)
|
||||
val newFiles = mutableSetOf<KotlinSourceFile>()
|
||||
|
||||
val newFingerprints = HashMap<KotlinSourceFile, ICHash>(kotlinLibraryHeader.sourceFiles.size)
|
||||
kotlinLibraryHeader.sourceFiles.forEachIndexed { index, file -> newFingerprints[file] = library.fingerprint(index) }
|
||||
|
||||
val modifiedFiles = HashMap<KotlinSourceFile, KotlinSourceFileMetadata>(newFingerprints.size).apply {
|
||||
for ((file, fileNewFingerprint) in newFingerprints) {
|
||||
val oldFingerprint = cachedFingerprints[file]
|
||||
if (oldFingerprint == null) {
|
||||
newFiles += file
|
||||
}
|
||||
if (oldFingerprint != fileNewFingerprint) {
|
||||
val metadata = fetchSourceFileMetadata(file, false)
|
||||
put(file, metadata)
|
||||
}
|
||||
deletedFiles.remove(file)
|
||||
for ((file, fileNewFingerprint) in library.sourceFileFingerprints) {
|
||||
when (cachedFingerprints[file]) {
|
||||
fileNewFingerprint -> nonModifiedFiles.add(file)
|
||||
null -> addedFiles.add(file)
|
||||
else -> modifiedFiles[file] = fetchSourceFileMetadata(file, false)
|
||||
}
|
||||
}
|
||||
|
||||
val removedFiles = deletedFiles.associateWith { fetchSourceFileMetadata(it, false) }
|
||||
val removedFiles = (cachedFingerprints.keys - library.sourceFileFingerprints.keys).associateWith {
|
||||
fetchSourceFileMetadata(it, false)
|
||||
}
|
||||
|
||||
removedSrcFiles = deletedFiles
|
||||
newCacheHeader = CacheHeader(klibFileHash, libraryFile, newFingerprints)
|
||||
removedSrcFiles = removedFiles.keys
|
||||
cacheHeaderShouldBeUpdated = true
|
||||
|
||||
return ModifiedFiles(modifiedFiles, removedFiles, newFiles)
|
||||
return ModifiedFiles(addedFiles, removedFiles, modifiedFiles, nonModifiedFiles)
|
||||
}
|
||||
|
||||
fun fetchSourceFileFullMetadata(srcFile: KotlinSourceFile): KotlinSourceFileMetadata {
|
||||
@@ -161,7 +143,7 @@ internal class IncrementalCache(private val library: KotlinLibrary, val cacheDir
|
||||
kotlinLibrarySourceFileMetadata.getOrPut(srcFile) {
|
||||
val signatureToIndexMapping = signatureToIndexMappingFromMetadata.getOrPut(srcFile) { hashMapOf() }
|
||||
val deserializer: IdSignatureDeserializer by lazy {
|
||||
kotlinLibraryHeader.signatureDeserializers[srcFile] ?: notFoundIcError("signature deserializer", libraryFile, srcFile)
|
||||
library.sourceFileDeserializers[srcFile] ?: notFoundIcError("signature deserializer", library.libraryFile, srcFile)
|
||||
}
|
||||
|
||||
fun CodedInputStream.deserializeIdSignatureAndSave() = readIdSignature { index ->
|
||||
|
||||
+48
-14
@@ -10,23 +10,45 @@ import org.jetbrains.kotlin.backend.common.serialization.IrLibraryBytesSource
|
||||
import org.jetbrains.kotlin.backend.common.serialization.IrLibraryFileFromBytes
|
||||
import org.jetbrains.kotlin.backend.common.serialization.codedInputStream
|
||||
import org.jetbrains.kotlin.backend.common.serialization.proto.IrFile
|
||||
import org.jetbrains.kotlin.ir.backend.js.jsOutputName
|
||||
import org.jetbrains.kotlin.library.KotlinLibrary
|
||||
import org.jetbrains.kotlin.protobuf.ExtensionRegistryLite
|
||||
import java.io.File
|
||||
|
||||
internal class KotlinLibraryHeader(library: KotlinLibrary) {
|
||||
val sourceFiles: List<KotlinSourceFile>
|
||||
val signatureDeserializers: Map<KotlinSourceFile, IdSignatureDeserializer>
|
||||
internal interface KotlinLibraryHeader {
|
||||
val libraryFile: KotlinLibraryFile
|
||||
|
||||
init {
|
||||
val libraryFingerprint: ICHash?
|
||||
|
||||
val sourceFileDeserializers: Map<KotlinSourceFile, IdSignatureDeserializer>
|
||||
val sourceFileFingerprints: Map<KotlinSourceFile, ICHash>
|
||||
|
||||
val jsOutputName: String?
|
||||
}
|
||||
|
||||
internal class KotlinLoadedLibraryHeader(private val library: KotlinLibrary) : KotlinLibraryHeader {
|
||||
override val libraryFile: KotlinLibraryFile = KotlinLibraryFile(library)
|
||||
|
||||
override val libraryFingerprint: ICHash by lazy { File(libraryFile.path).fileHashForIC() }
|
||||
|
||||
override val sourceFileDeserializers: Map<KotlinSourceFile, IdSignatureDeserializer> by lazy { sourceFiles.first }
|
||||
override val sourceFileFingerprints: Map<KotlinSourceFile, ICHash> by lazy { sourceFiles.second }
|
||||
|
||||
override val jsOutputName: String?
|
||||
get() = library.jsOutputName
|
||||
|
||||
private val sourceFiles by lazy {
|
||||
val filesCount = library.fileCount()
|
||||
val extReg = ExtensionRegistryLite.newInstance()
|
||||
val files = buildListUntil(filesCount) {
|
||||
val fileProto = IrFile.parseFrom(library.file(it).codedInputStream, extReg)
|
||||
add(KotlinSourceFile(fileProto.fileEntry.name))
|
||||
}
|
||||
|
||||
val deserializers = buildMapUntil(filesCount) {
|
||||
put(files[it], IdSignatureDeserializer(IrLibraryFileFromBytes(object : IrLibraryBytesSource() {
|
||||
val deserializers = HashMap<KotlinSourceFile, IdSignatureDeserializer>(filesCount)
|
||||
val fingerprints = HashMap<KotlinSourceFile, ICHash>(filesCount)
|
||||
|
||||
repeat(filesCount) {
|
||||
val fileProto = IrFile.parseFrom(library.file(it).codedInputStream, extReg)
|
||||
val srcFile = KotlinSourceFile(fileProto.fileEntry.name)
|
||||
|
||||
deserializers[srcFile] = IdSignatureDeserializer(IrLibraryFileFromBytes(object : IrLibraryBytesSource() {
|
||||
private fun err(): Nothing = icError("Not supported")
|
||||
override fun irDeclaration(index: Int): ByteArray = err()
|
||||
override fun type(index: Int): ByteArray = err()
|
||||
@@ -34,10 +56,22 @@ internal class KotlinLibraryHeader(library: KotlinLibrary) {
|
||||
override fun string(index: Int): ByteArray = library.string(index, it)
|
||||
override fun body(index: Int): ByteArray = err()
|
||||
override fun debugInfo(index: Int): ByteArray? = null
|
||||
}), null))
|
||||
}
|
||||
}), null)
|
||||
|
||||
sourceFiles = files
|
||||
signatureDeserializers = deserializers
|
||||
fingerprints[srcFile] = library.fingerprint(it)
|
||||
}
|
||||
deserializers to fingerprints
|
||||
}
|
||||
}
|
||||
|
||||
internal class KotlinRemovedLibraryHeader(private val libCacheDir: File) : KotlinLibraryHeader {
|
||||
override val libraryFile: KotlinLibraryFile
|
||||
get() = icError("removed library name is unavailable; cache dir: ${libCacheDir.absolutePath}")
|
||||
|
||||
override val libraryFingerprint: ICHash? get() = null
|
||||
|
||||
override val sourceFileDeserializers: Map<KotlinSourceFile, IdSignatureDeserializer> get() = emptyMap()
|
||||
override val sourceFileFingerprints: Map<KotlinSourceFile, ICHash> get() = emptyMap()
|
||||
|
||||
override val jsOutputName: String? get() = null
|
||||
}
|
||||
|
||||
+6
-1
@@ -98,9 +98,14 @@ abstract class KotlinSourceFileExports {
|
||||
|
||||
abstract class KotlinSourceFileMetadata : KotlinSourceFileExports() {
|
||||
abstract val directDependencies: KotlinSourceFileMap<Map<IdSignature, ICHash>>
|
||||
|
||||
fun isEmpty() = inverseDependencies.isEmpty() && directDependencies.isEmpty()
|
||||
}
|
||||
|
||||
fun KotlinSourceFileMetadata.isEmpty() = inverseDependencies.isEmpty() && directDependencies.isEmpty()
|
||||
internal object KotlinSourceFileMetadataNotExist : KotlinSourceFileMetadata() {
|
||||
override val inverseDependencies = KotlinSourceFileMap<Set<IdSignature>>(emptyMap())
|
||||
override val directDependencies = KotlinSourceFileMap<Map<IdSignature, ICHash>>(emptyMap())
|
||||
}
|
||||
|
||||
internal class DirtyFileExports(
|
||||
override val inverseDependencies: KotlinSourceFileMutableMap<Set<IdSignature>> = KotlinSourceFileMutableMap()
|
||||
|
||||
@@ -42,12 +42,12 @@ import java.util.EnumSet
|
||||
|
||||
abstract class AbstractInvalidationTest : KotlinTestWithEnvironment() {
|
||||
companion object {
|
||||
private val TEST_DATA_DIR_PATH = System.getProperty("kotlin.js.test.root.out.dir") ?: error("'kotlin.js.test.root.out.dir' is not set")
|
||||
private const val BOX_FUNCTION_NAME = "box"
|
||||
|
||||
private const val STDLIB_MODULE_NAME = "kotlin-kotlin-stdlib-js-ir"
|
||||
private val OUT_DIR_PATH = System.getProperty("kotlin.js.test.root.out.dir") ?: error("'kotlin.js.test.root.out.dir' is not set")
|
||||
private val STDLIB_KLIB = File(System.getProperty("kotlin.js.stdlib.klib.path") ?: error("Please set stdlib path")).canonicalPath
|
||||
|
||||
private const val BOX_FUNCTION_NAME = "box"
|
||||
private const val STDLIB_MODULE_NAME = "kotlin-kotlin-stdlib-js-ir"
|
||||
|
||||
private val KT_FILE_IGNORE_PATTERN = Regex("^.*\\..+\\.kt$")
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ abstract class AbstractInvalidationTest : KotlinTestWithEnvironment() {
|
||||
val expectedFileStats: Map<String, Set<String>>
|
||||
)
|
||||
|
||||
private fun setupTestStep(projStep: ProjectInfo.ProjectBuildStep, module: String): TestStepInfo {
|
||||
private fun setupTestStep(projStep: ProjectInfo.ProjectBuildStep, module: String, buildKlib: Boolean): TestStepInfo {
|
||||
val projStepId = projStep.id
|
||||
val moduleTestDir = File(testDir, module)
|
||||
val moduleSourceDir = File(sourceDir, module)
|
||||
@@ -136,17 +136,20 @@ abstract class AbstractInvalidationTest : KotlinTestWithEnvironment() {
|
||||
modification.execute(moduleTestDir, moduleSourceDir) { deletedFiles.add(it.name) }
|
||||
}
|
||||
|
||||
val dependencies = moduleStep.dependencies.mapTo(mutableListOf(File(STDLIB_KLIB))) {
|
||||
resolveModuleArtifact(it.moduleName, buildDir)
|
||||
val expectedFileStats = moduleStep.expectedFileStats.toMutableMap()
|
||||
if (deletedFiles.isNotEmpty()) {
|
||||
val removedFiles = expectedFileStats[DirtyFileState.REMOVED_FILE.str] ?: emptySet()
|
||||
expectedFileStats[DirtyFileState.REMOVED_FILE.str] = removedFiles + deletedFiles
|
||||
}
|
||||
val outputKlibFile = resolveModuleArtifact(module, buildDir)
|
||||
val configuration = createConfiguration(module, projStep.language)
|
||||
buildArtifact(configuration, module, moduleSourceDir, dependencies, outputKlibFile)
|
||||
|
||||
val expectedFileStats = if (deletedFiles.isEmpty()) {
|
||||
moduleStep.expectedFileStats
|
||||
} else {
|
||||
moduleStep.expectedFileStats + (DirtyFileState.REMOVED_FILE.str to deletedFiles)
|
||||
val outputKlibFile = resolveModuleArtifact(module, buildDir)
|
||||
|
||||
if (buildKlib) {
|
||||
val dependencies = moduleStep.dependencies.mapTo(mutableListOf(File(STDLIB_KLIB))) {
|
||||
resolveModuleArtifact(it.moduleName, buildDir)
|
||||
}
|
||||
val configuration = createConfiguration(module, projStep.language)
|
||||
buildArtifact(configuration, module, moduleSourceDir, dependencies, outputKlibFile)
|
||||
}
|
||||
return TestStepInfo(
|
||||
module.safeModuleName,
|
||||
@@ -155,9 +158,7 @@ abstract class AbstractInvalidationTest : KotlinTestWithEnvironment() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun verifyCacheUpdateStats(
|
||||
stepId: Int, stats: KotlinSourceFileMap<EnumSet<DirtyFileState>>, testInfo: List<TestStepInfo>
|
||||
) {
|
||||
private fun verifyCacheUpdateStats(stepId: Int, stats: KotlinSourceFileMap<EnumSet<DirtyFileState>>, testInfo: List<TestStepInfo>) {
|
||||
val gotStats = stats.filter { it.key.path != STDLIB_KLIB }
|
||||
|
||||
val checkedLibs = mutableSetOf<KotlinLibraryFile>()
|
||||
@@ -170,7 +171,9 @@ abstract class AbstractInvalidationTest : KotlinTestWithEnvironment() {
|
||||
val got = mutableMapOf<String, MutableSet<String>>()
|
||||
for ((srcFile, dirtyStats) in updateStatus) {
|
||||
for (dirtyStat in dirtyStats) {
|
||||
got.getOrPut(dirtyStat.str) { mutableSetOf() }.add(File(srcFile.path).name)
|
||||
if (dirtyStat != DirtyFileState.NON_MODIFIED_IR) {
|
||||
got.getOrPut(dirtyStat.str) { mutableSetOf() }.add(File(srcFile.path).name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +226,7 @@ abstract class AbstractInvalidationTest : KotlinTestWithEnvironment() {
|
||||
|
||||
fun execute() {
|
||||
for (projStep in projectInfo.steps) {
|
||||
val testInfo = projStep.order.map { setupTestStep(projStep, it) }
|
||||
val testInfo = projStep.order.map { setupTestStep(projStep, it, true) }
|
||||
|
||||
val configuration = createConfiguration(projStep.order.last(), projStep.language)
|
||||
val cacheUpdater = CacheUpdater(
|
||||
@@ -238,8 +241,10 @@ abstract class AbstractInvalidationTest : KotlinTestWithEnvironment() {
|
||||
}
|
||||
)
|
||||
|
||||
val removedModulesInfo = (projectInfo.modules - projStep.order.toSet()).map { setupTestStep(projStep, it, false) }
|
||||
|
||||
val icCaches = cacheUpdater.actualizeCaches()
|
||||
verifyCacheUpdateStats(projStep.id, cacheUpdater.getDirtyFileLastStats(), testInfo)
|
||||
verifyCacheUpdateStats(projStep.id, cacheUpdater.getDirtyFileLastStats(), testInfo + removedModulesInfo)
|
||||
|
||||
val mainModuleName = icCaches.last().moduleExternalName
|
||||
val jsExecutableProducer = JsExecutableProducer(
|
||||
@@ -342,7 +347,7 @@ abstract class AbstractInvalidationTest : KotlinTestWithEnvironment() {
|
||||
}
|
||||
|
||||
private fun testWorkingDir(testName: String): File {
|
||||
val dir = File(File(File(TEST_DATA_DIR_PATH), "incrementalOut/invalidation"), testName)
|
||||
val dir = File(File(File(OUT_DIR_PATH), "incrementalOut/invalidation"), testName)
|
||||
|
||||
dir.invalidateDir()
|
||||
|
||||
|
||||
+10
@@ -275,11 +275,21 @@ public class InvalidationTestGenerated extends AbstractInvalidationTest {
|
||||
runTest("js/js.translator/testData/incremental/invalidation/renameFile/");
|
||||
}
|
||||
|
||||
@TestMetadata("renameModule")
|
||||
public void testRenameModule() throws Exception {
|
||||
runTest("js/js.translator/testData/incremental/invalidation/renameModule/");
|
||||
}
|
||||
|
||||
@TestMetadata("simple")
|
||||
public void testSimple() throws Exception {
|
||||
runTest("js/js.translator/testData/incremental/invalidation/simple/");
|
||||
}
|
||||
|
||||
@TestMetadata("splitJoinModule")
|
||||
public void testSplitJoinModule() throws Exception {
|
||||
runTest("js/js.translator/testData/incremental/invalidation/splitJoinModule/");
|
||||
}
|
||||
|
||||
@TestMetadata("suspendFunctions")
|
||||
public void testSuspendFunctions() throws Exception {
|
||||
runTest("js/js.translator/testData/incremental/invalidation/suspendFunctions/");
|
||||
|
||||
Vendored
+2
@@ -5,7 +5,9 @@ STEP 1:
|
||||
STEP 2:
|
||||
updated exports: l1a.kt
|
||||
STEP 3:
|
||||
removed inverse depends: l1b.kt, l1a.kt
|
||||
STEP 4:
|
||||
modifications:
|
||||
U : l1a.4.kt -> l1a.kt
|
||||
modified ir: l1a.kt
|
||||
updated exports: l1b.kt
|
||||
|
||||
Vendored
+1
-5
@@ -1,8 +1,4 @@
|
||||
STEP 0:
|
||||
modifications:
|
||||
U : proxy.0.kt -> proxy.kt
|
||||
added file: proxy.kt
|
||||
STEP 1..7:
|
||||
STEP 0..7:
|
||||
STEP 8:
|
||||
dependencies: lib1
|
||||
modifications:
|
||||
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
fun unused_foo_proxy(s: String) = 77
|
||||
+1
@@ -0,0 +1 @@
|
||||
fun foo() = 1
|
||||
+1
@@ -0,0 +1 @@
|
||||
inline fun foo() = 2
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
STEP 0:
|
||||
STEP 1:
|
||||
modifications:
|
||||
U : l1.1.kt -> l1.kt
|
||||
added file: l1.kt
|
||||
STEP 2:
|
||||
modifications:
|
||||
U : l1.2.kt -> l1.kt
|
||||
modified ir: l1.kt
|
||||
STEP 3:
|
||||
removed file: l1.kt
|
||||
STEP 4..5:
|
||||
@@ -0,0 +1 @@
|
||||
fun foo() = 0
|
||||
@@ -0,0 +1 @@
|
||||
fun foo() = 3
|
||||
@@ -0,0 +1 @@
|
||||
inline fun foo() = 4
|
||||
@@ -0,0 +1 @@
|
||||
inline fun foo() = 5
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
STEP 0:
|
||||
modifications:
|
||||
U : l1.0.kt -> l1.kt
|
||||
added file: l1.kt
|
||||
STEP 1:
|
||||
removed file: l1.kt
|
||||
STEP 2:
|
||||
STEP 3:
|
||||
modifications:
|
||||
U : l1.3.kt -> l1.kt
|
||||
added file: l1.kt
|
||||
STEP 4:
|
||||
modifications:
|
||||
U : l1.4.kt -> l1.kt
|
||||
modified ir: l1.kt
|
||||
STEP 5:
|
||||
modifications:
|
||||
U : l1.5.kt -> l1.kt
|
||||
modified ir: l1.kt
|
||||
@@ -0,0 +1,7 @@
|
||||
fun box(stepId: Int): String {
|
||||
val x = foo()
|
||||
if (x != stepId) {
|
||||
return "Fail: $x != $stepId"
|
||||
}
|
||||
return "OK"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
STEP 0:
|
||||
dependencies: lib1
|
||||
added file: m.kt
|
||||
STEP 1:
|
||||
dependencies: lib1-new
|
||||
removed direct depends: m.kt
|
||||
STEP 2:
|
||||
dependencies: lib1-new
|
||||
updated imports: m.kt
|
||||
STEP 3:
|
||||
dependencies: lib1
|
||||
removed direct depends: m.kt
|
||||
STEP 4..5:
|
||||
dependencies: lib1
|
||||
updated imports: m.kt
|
||||
@@ -0,0 +1,11 @@
|
||||
MODULES: lib1, lib1-new, main
|
||||
|
||||
STEP 0:
|
||||
libs: lib1, main
|
||||
dirty js: lib1, main
|
||||
STEP 1..2:
|
||||
libs: lib1-new, main
|
||||
dirty js: lib1-new, main
|
||||
STEP 3..5:
|
||||
libs: lib1, main
|
||||
dirty js: lib1, main
|
||||
+1
@@ -0,0 +1 @@
|
||||
fun fooA() = 1
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
STEP 0:
|
||||
STEP 1:
|
||||
modifications:
|
||||
U : l1a.1.kt -> l1a.kt
|
||||
added file: l1a.kt
|
||||
STEP 2..3:
|
||||
STEP 4:
|
||||
modifications:
|
||||
D : l1a.kt
|
||||
STEP 5:
|
||||
+1
@@ -0,0 +1 @@
|
||||
fun fooB() = 2
|
||||
+1
@@ -0,0 +1 @@
|
||||
inline fun fooB() = 3
|
||||
+1
@@ -0,0 +1 @@
|
||||
inline fun fooB() = 4
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
STEP 0:
|
||||
STEP 1:
|
||||
modifications:
|
||||
U : l1b.1.kt -> l1b.kt
|
||||
added file: l1b.kt
|
||||
STEP 2:
|
||||
modifications:
|
||||
U : l1b.2.kt -> l1b.kt
|
||||
modified ir: l1b.kt
|
||||
STEP 2:
|
||||
modifications:
|
||||
U : l1b.3.kt -> l1b.kt
|
||||
modified ir: l1b.kt
|
||||
STEP 4:
|
||||
modifications:
|
||||
D : l1b.kt
|
||||
STEP 5:
|
||||
+1
@@ -0,0 +1 @@
|
||||
fun fooC() = 4
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
STEP 0:
|
||||
STEP 1:
|
||||
modifications:
|
||||
U : l1c.1.kt -> l1c.kt
|
||||
added file: l1c.kt
|
||||
STEP 2..3:
|
||||
STEP 4:
|
||||
modifications:
|
||||
D : l1c.kt
|
||||
STEP 5:
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
fun fooA() = 3
|
||||
|
||||
fun fooB() = 3
|
||||
|
||||
fun fooC() = 4
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
fun fooA() = 3
|
||||
|
||||
fun fooB() = 4
|
||||
|
||||
fun fooC() = 4
|
||||
+1
@@ -0,0 +1 @@
|
||||
fun fooA() = 1
|
||||
+1
@@ -0,0 +1 @@
|
||||
fun fooB() = 2
|
||||
+1
@@ -0,0 +1 @@
|
||||
fun fooC() = 3
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
STEP 0:
|
||||
modifications:
|
||||
U : l1a.0.kt -> l1a.kt
|
||||
U : l1b.0.kt -> l1b.kt
|
||||
U : l1c.0.kt -> l1c.kt
|
||||
added file: l1a.kt, l1b.kt, l1c.kt
|
||||
STEP 1:
|
||||
modifications:
|
||||
D : l1a.kt
|
||||
D : l1b.kt
|
||||
D : l1c.kt
|
||||
STEP 2..3:
|
||||
STEP 4:
|
||||
modifications:
|
||||
U : l1.4.kt -> l1.kt
|
||||
added file: l1.kt
|
||||
STEP 5:
|
||||
modifications:
|
||||
U : l1.5.kt -> l1.kt
|
||||
modified ir: l1.kt
|
||||
+1
@@ -0,0 +1 @@
|
||||
fun qux() = fooA() + fooB() + fooC() - 6
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
STEP 0:
|
||||
dependencies: lib1
|
||||
modifications:
|
||||
U : l2.0.kt -> l2.kt
|
||||
added file: l2.kt
|
||||
STEP 1:
|
||||
dependencies: lib1-a, lib1-b, lib1-c
|
||||
removed direct depends: l2.kt
|
||||
STEP 2..3:
|
||||
dependencies: lib1-a, lib1-b, lib1-c
|
||||
updated imports: l2.kt
|
||||
STEP 4:
|
||||
dependencies: lib1
|
||||
removed direct depends: l2.kt
|
||||
STEP 5:
|
||||
dependencies: lib1
|
||||
@@ -0,0 +1,7 @@
|
||||
fun box(stepId: Int): String {
|
||||
val x = qux()
|
||||
if (x != stepId) {
|
||||
return "Fail: $x != $stepId"
|
||||
}
|
||||
return "OK"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
STEP 0:
|
||||
dependencies: lib1, lib2
|
||||
added file: m.kt
|
||||
STEP 1..3:
|
||||
dependencies: lib1-a, lib1-b, lib1-c, lib2
|
||||
STEP 4..5:
|
||||
dependencies: lib1, lib2
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
MODULES: lib1, lib1-a, lib1-b, lib1-c, lib2, main
|
||||
|
||||
STEP 0:
|
||||
libs: lib1, lib2, main
|
||||
dirty js: lib1, lib2, main
|
||||
STEP 1:
|
||||
libs: lib1-a, lib1-b, lib1-c, lib2, main
|
||||
dirty js: lib1-a, lib1-b, lib1-c, lib2
|
||||
STEP 2..3:
|
||||
libs: lib1-a, lib1-b, lib1-c, lib2, main
|
||||
dirty js: lib1-b, lib2
|
||||
STEP 4:
|
||||
libs: lib1, lib2, main
|
||||
dirty js: lib1, lib2
|
||||
STEP 5:
|
||||
libs: lib1, lib2, main
|
||||
dirty js: lib1
|
||||
Reference in New Issue
Block a user