get rid of intellij.build.toolbox.litegen parameter and use BuildOptions.TOOLBOX_LITE...
[idea/community.git] / platform / build-scripts / icons / src / org / jetbrains / intellij / build / images / sync / checkIcons.kt
1 // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package org.jetbrains.intellij.build.images.sync
3
4 import org.jetbrains.intellij.build.images.ImageExtension
5 import org.jetbrains.intellij.build.images.isImage
6 import java.io.File
7 import java.nio.file.Files
8 import java.util.function.Consumer
9 import java.util.stream.Collectors
10 import java.util.stream.Stream
11 import kotlin.streams.toList
12
13 fun main(args: Array<String>) {
14   if (args.isNotEmpty()) System.setProperty(Context.iconsCommitHashesToSyncArg, args.joinToString())
15   checkIcons()
16 }
17
18 internal fun checkIcons(context: Context = Context(), loggerImpl: Consumer<String> = Consumer(::println)) {
19   System.setProperty("java.awt.headless", "true")
20   logger = loggerImpl
21   context.iconsRepo = findGitRepoRoot(context.iconsRepoDir)
22   context.devRepoRoot = findGitRepoRoot(context.devRepoDir)
23   val devRepoVcsRoots = vcsRoots(context.devRepoRoot)
24   callWithTimer("Searching for changed icons..") {
25     when {
26       context.iconsCommitHashesToSync.isNotEmpty() -> searchForChangedIconsByDesigners(context)
27       context.devIconsCommitHashesToSync.isNotEmpty() -> searchForChangedIconsByDev(context, devRepoVcsRoots)
28       else -> {
29         context.icons = readIconsRepo(context)
30         context.devIcons = readDevRepo(context, devRepoVcsRoots)
31         searchForAllChangedIcons(context, devRepoVcsRoots)
32       }
33     }
34   }
35   syncDevRepo(context)
36   if (!context.devIconsSyncAll && !context.iconsSyncRequired() && !context.devSyncRequired()) {
37     if (isUnderTeamCity() && isPreviousBuildFailed()) {
38       context.doFail("No changes are found")
39     }
40     else log("No changes are found")
41   }
42   else if (isUnderTeamCity()) {
43     findCommitsToSync(context)
44     createReviews(context)
45     val investigator = if (context.isFail() && context.assignInvestigation) {
46       assignInvestigation(context)
47     }
48     else null
49     if (context.notifySlack) sendNotification(investigator, context)
50   }
51   syncIconsRepo(context)
52   val report = report(context, skippedDirs.size)
53   if (isUnderTeamCity() &&
54       (context.isFail() ||
55        // partial sync shouldn't make build successful
56        context.devIconsCommitHashesToSync.isNotEmpty() && isPreviousBuildFailed() ||
57        // reviews should be created
58        context.iconsCommitHashesToSync.isNotEmpty() && context.devReviews().isEmpty())) context.doFail(report)
59   else log(report)
60 }
61
62 private enum class SearchType { MODIFIED, REMOVED_BY_DEV, REMOVED_BY_DESIGNERS }
63
64 private fun searchForAllChangedIcons(context: Context, devRepoVcsRoots: Collection<File>) {
65   log("Searching for all")
66   val devIconsTmp = HashMap(context.devIcons)
67   val modified = mutableListOf<String>()
68   context.icons.forEach { (icon, gitObject) ->
69     when {
70       !devIconsTmp.containsKey(icon) -> context.byDesigners.added += icon
71       gitObject.hash != devIconsTmp[icon]?.hash -> modified += icon
72       else -> context.consistent += icon
73     }
74     devIconsTmp.remove(icon)
75   }
76   context.byDev.added += devIconsTmp.keys
77   Stream.of(
78     { SearchType.MODIFIED to modifiedByDev(context, modified) },
79     { SearchType.REMOVED_BY_DEV to removedByDev(context, context.byDesigners.added, devRepoVcsRoots, context.devRepoDir) },
80     {
81       val iconsDir = context.iconsRepoDir.relativeTo(context.iconsRepo).path.let { if (it.isEmpty()) "" else "$it/" }
82       SearchType.REMOVED_BY_DESIGNERS to removedByDesigners(context, context.byDev.added, context.iconsRepo, iconsDir)
83     }
84   ).parallel().map { it() }.toList().forEach {
85     val (searchType, searchResult) = it
86     when (searchType) {
87       SearchType.MODIFIED -> {
88         context.byDev.modified += searchResult
89         context.byDesigners.modified += modified.filter { file ->
90           !context.byDev.modified.contains(file)
91         }.toMutableList()
92       }
93       SearchType.REMOVED_BY_DEV -> {
94         context.byDev.removed += searchResult
95         context.byDesigners.added.removeAll(searchResult)
96       }
97       SearchType.REMOVED_BY_DESIGNERS -> {
98         context.byDesigners.removed += searchResult
99         context.byDev.added.removeAll(searchResult)
100       }
101     }
102   }
103 }
104
105 private fun searchForChangedIconsByDesigners(context: Context) {
106   if (!isUnderTeamCity()) gitPull(context.iconsRepo)
107   fun asIcons(files: Collection<String>) = files
108     .filter { ImageExtension.fromName(it) != null }
109     .map { context.iconsRepo.resolve(it).toRelativeString(context.iconsRepoDir) }
110   ArrayList(context.iconsCommitHashesToSync).map {
111     commitInfo(context.iconsRepo, it) ?: error("Commit $it is not found in ${context.iconsRepoName}")
112   }.sortedBy { it.timestamp }.forEach {
113     val commit = it.hash
114     val before = context.iconsChanges().size
115     changesFromCommit(context.iconsRepo, commit).forEach { (type, files) ->
116       context.byDesigners.register(type, asIcons(files))
117     }
118     if (context.iconsChanges().size == before) {
119       log("No icons in $commit, skipping")
120       context.iconsCommitHashesToSync.remove(commit)
121     }
122   }
123   log("Found ${context.iconsCommitHashesToSync.size} commits to sync from ${context.iconsRepoName} to ${context.devRepoName}")
124   log(context.iconsCommitHashesToSync.joinToString())
125 }
126
127 private fun searchForChangedIconsByDev(context: Context, devRepoVcsRoots: List<File>) {
128   fun asIcons(files: Collection<String>, repo: File) = files.asSequence()
129     .filter { ImageExtension.fromName(it) != null }
130     .map(repo::resolve)
131     .filter(context.devIconsFilter)
132     .map { it.toRelativeString(context.devRepoRoot) }.toList()
133   ArrayList(context.devIconsCommitHashesToSync).mapNotNull { commit ->
134     devRepoVcsRoots.asSequence().map { repo ->
135       try {
136         commitInfo(repo, commit)
137       }
138       catch (ignored: Exception) {
139         null
140       }
141     }.filterNotNull().firstOrNull().apply {
142       if (this == null) {
143         log("No repo is found for $commit, skipping")
144         context.devIconsCommitHashesToSync.remove(commit)
145       }
146     }
147   }.sortedBy { it.timestamp }.forEach {
148     val commit = it.hash
149     val before = context.devChanges().size
150     changesFromCommit(it.repo, commit).forEach { type, files ->
151       context.byDev.register(type, asIcons(files, it.repo))
152     }
153     if (context.devChanges().size == before) {
154       log("No icons in $commit, skipping")
155       context.devIconsCommitHashesToSync.remove(commit)
156     }
157   }
158   log("Found ${context.devIconsCommitHashesToSync.size} commits to sync from ${context.devRepoName} to ${context.iconsRepoName}")
159   log(context.devIconsCommitHashesToSync.joinToString())
160 }
161
162 private fun readIconsRepo(context: Context) = protectStdErr {
163   val (iconsRepo, iconsRepoDir) = context.iconsRepo to context.iconsRepoDir
164   listGitObjects(iconsRepo, iconsRepoDir) { file ->
165     // read icon hashes
166     Icon(file).isValid
167   }.also {
168     if (it.isEmpty()) error("${context.iconsRepoName} repo doesn't contain icons")
169   }
170 }
171
172 private fun readDevRepo(context: Context, devRepoVcsRoots: List<File>) = protectStdErr {
173   if (context.skipDirsPattern != null) {
174     log("Using pattern ${context.skipDirsPattern} to skip dirs")
175   }
176   val devIcons = if (devRepoVcsRoots.size == 1 && devRepoVcsRoots.contains(context.devRepoRoot)) {
177     // read icons from devRepoRoot
178     listGitObjects(context.devRepoRoot, context.devRepoDir, context.devIconsFilter)
179   }
180   else {
181     // read icons from multiple repos in devRepoRoot
182     listGitObjects(context.devRepoRoot, devRepoVcsRoots, context.devIconsFilter)
183   }
184   if (devIcons.isEmpty()) error("${context.devRepoName} doesn't contain icons")
185   devIcons.toMutableMap()
186 }
187
188 internal fun filterDevIcon(file: File, testRoots: Set<File>, skipDirsRegex: Regex?, context: Context): Boolean {
189   val path = file.toPath()
190   if (!isImage(path) || doSkip(file, testRoots, skipDirsRegex)) return false
191   val icon = Icon(file)
192   return icon.isValid ||
193          // if not exists then check respective icon in icons repo
194          !Files.exists(path) && Icon(context.iconsRepoDir.resolve(file.toRelativeString(context.devRepoRoot))).isValid ||
195          IconRobotsDataReader.isSyncForced(file)
196 }
197
198 @Volatile
199 private var skippedDirs = emptySet<File>()
200 private var skippedDirsGuard = Any()
201
202 private fun doSkip(file: File, testRoots: Set<File>, skipDirsRegex: Regex?): Boolean {
203   val skipDir = (file.isDirectory || !file.exists()) &&
204                 // is test root
205                 (testRoots.contains(file) ||
206                  // or matches skip dir pattern
207                  skipDirsRegex != null && file.name.matches(skipDirsRegex))
208   if (skipDir) synchronized(skippedDirsGuard) {
209     skippedDirs += file
210   }
211   return skipDir ||
212          // or sync skipped in icon-robots.txt
213          IconRobotsDataReader.isSyncSkipped(file) ||
214          // or check parent
215          file.parentFile != null && doSkip(file.parentFile, testRoots, skipDirsRegex)
216 }
217
218 private fun removedByDesigners(context: Context, addedByDev: Collection<String>,
219                                iconsRepo: File, iconsDir: String) = addedByDev.parallelStream().filter {
220   val byDesigners = latestChangeTime("$iconsDir$it", iconsRepo)
221   // latest changes are made by designers
222   val latestChangeTime = latestChangeTime(context.devIcons[it])
223   latestChangeTime > 0 && byDesigners > 0 && latestChangeTime < byDesigners
224 }.toList()
225
226 private fun removedByDev(context: Context,
227                          addedByDesigners: Collection<String>,
228                          devRepos: Collection<File>,
229                          devRepoDir: File) = addedByDesigners.parallelStream().filter {
230   val byDev = latestChangeTime(File(devRepoDir, it).absolutePath, devRepos)
231   // latest changes are made by developers
232   byDev > 0 && latestChangeTime(context.icons[it]) < byDev
233 }.toList()
234
235 private fun latestChangeTime(file: String, repos: Collection<File>): Long {
236   for (repo in repos) {
237     val prefix = "${repo.absolutePath}/"
238     if (file.startsWith(prefix)) {
239       val lct = latestChangeTime(file.removePrefix(prefix), repo)
240       if (lct > 0) return lct
241     }
242   }
243   return -1
244 }
245
246 private fun modifiedByDev(context: Context, modified: Collection<String>) = modified.parallelStream().filter {
247   // latest changes are made by developers
248   val latestChangeTimeByDev = latestChangeTime(context.devIcons[it])
249   latestChangeTimeByDev > 0 && latestChangeTime(context.icons[it]) < latestChangeTimeByDev
250 }.collect(Collectors.toList())
251
252 private fun latestChangeTime(obj: GitObject?) = latestChangeTime(obj!!.path, obj.repo)