#!/usr/bin/env kotlin /** * This script is used to update the Android Studio releases page. * At first, it fetches the list of Android Studio updates from the official `updates.xml` file. * Parsed list is used to generate the Markdown table. * The actual IntelliJ IDEA release version is obtained with the help of the JetBrains Data Services API. */ @file:DependsOn("org.jsoup:jsoup:1.14.3") @file:DependsOn("net.swiftzer.semver:semver:1.1.2") @file:DependsOn("org.simpleframework:simple-xml:2.7.1") @file:DependsOn("org.json:json:20211205") import net.swiftzer.semver.SemVer import org.jsoup.Jsoup import org.simpleframework.xml.Attribute import org.simpleframework.xml.ElementList import org.simpleframework.xml.Root import org.simpleframework.xml.core.Persister import java.io.BufferedInputStream import java.io.File import java.io.FileOutputStream import java.net.URL import java.util.zip.ZipFile val RELEASES_FILE_PATH_MD = "topics/_generated/android_studio_releases.md" val RELEASES_FILE_PATH_XML = "topics/_generated/android_studio_releases.xml" val INTELLIJ_RELEASES = "https://www.jetbrains.com/intellij-repository/releases/" val ANDROID_STUDIO_HOST = "https://developer.android.com" val CHANNEL_BADGES_LIST = """ [release]: https://img.shields.io/badge/-release-blue?style=flat-square [patch]: https://img.shields.io/badge/-patch-orange?style=flat-square [rc]: https://img.shields.io/badge/-rc-red?style=flat-square [beta]: https://img.shields.io/badge/-beta-darkred?style=flat-square [canary]: https://img.shields.io/badge/-canary-lightgrey?style=flat-square [preview]: https://img.shields.io/badge/-preview-darktgrey?style=flat-square """ val platformBuildToVersionMapping = INTELLIJ_RELEASES.fetch { content -> Jsoup.parse(content, "").select("h2:contains(com.jetbrains.intellij.idea) + table tbody tr").mapNotNull { tr -> val (version, build) = tr.select("td:nth-child(odd)").map { SemVer.parse(it.text()) } (build to version).takeIf { version.major > 2000 } }.toMap().toSortedMap() } val frameUrl = "$ANDROID_STUDIO_HOST/studio/archive".fetch { content -> Jsoup.parse(content, "").select("devsite-iframe iframe[src]").firstOrNull()?.attr("src") }.let { "$ANDROID_STUDIO_HOST/$it" } frameUrl.fetch { content -> val contentFile = file(RELEASES_FILE_PATH_XML) val current = contentFile.takeIf { it.length() > 0 }?.let { Persister().read(Content::class.java, it) } ?: Content() val nameToBuildMapping = current.items.associate { it.name to it.build } Jsoup.parse(content, "").select("section.expandable").run { mapIndexed { index, item -> val title = item.select("p").firstOrNull()?.text() ?: throw IllegalStateException("No title found") val (name, channelRaw, date) = """^([\w ]+ \(?[\d.]+\)? ?(?:(\w+) \d+)?) (\w+ \d+, \d+)$""".toRegex().find(title)?.groupValues?.drop(1) ?: emptyList() println("# $name") println(" ${index + 1}/$size") val href = item.select(".downloads a[href$=.zip]").firstOrNull()?.attr("href") val version = href?.split('/')?.let { it[it.indexOf("ide-zips") + 1] } ?: throw IllegalStateException("No version found for $name") val build = nameToBuildMapping[name].takeUnless(String?::isNullOrBlank) ?: run { href.resolveBuild() } val platformBuild = build.split('-').last().toLooseVersion() val platformVersion = platformBuildToVersionMapping[platformBuild] ?: run { platformBuildToVersionMapping.entries.findLast { it.key < platformBuild }?.value } val channel = channelRaw.takeIf { it.isNotBlank() } ?: "Release" println(" version='${version}'") println(" build='${build}'") println(" platformBuild='${platformBuild}'") println(" platformVersion='${platformVersion}'") Item(name, build, version, channel, platformBuild.toString(), platformVersion.toString(), date) } }.let { val version = with(current) { when (items.hashCode() != it.hashCode()) { true -> version + 1 false -> version } } Content(version, it) }.also { Persister().write(it, contentFile) }.also { (_, items) -> (""" """ + items.groupBy { it.version.toLooseVersion().major }.entries.joinToString("\n\n") { """ ## ${it.key}.* ${it.value.renderTable()} """ } + """ $CHANNEL_BADGES_LIST ${items.distinctBy(Item::version).take(5).renderTable()} $CHANNEL_BADGES_LIST """).split("\n").joinToString("\n", transform = String::trim).let(file(RELEASES_FILE_PATH_MD)::writeText) } } fun List.renderTable() = """ | Release Name | Channel | Release Date | Version | IntelliJ IDEA Version | |--------------|:-------:|--------------|---------|-----------------------| """ + sortedByDescending { it.version.toLooseVersion() }.joinToString("\n") { val name = it.name.removePrefix("Android Studio").trim() val channel = it.channel.lowercase().run { "![$this][$this]" } val date = it.date val version = "**${it.version}**
${it.build}" val platform = "**${it.platformVersion}**
${it.platformBuild}" "| $name | $channel | $date | $version | $platform |" } fun String.fetch(block: (String) -> T) = URL(this).openStream().use { inputStream -> block(inputStream.readBytes().toString(Charsets.UTF_8)) } fun String.download(block: (File) -> T) = URL(this).openStream().use { inputStream -> BufferedInputStream(inputStream).use { bis -> File.createTempFile("android-studio", ".zip").also(File::deleteOnExit).let { tempFile -> FileOutputStream(tempFile).use { outputStream -> println(" Downloading $this to $tempFile") ByteArray(1024).let { data -> var count: Int while (bis.read(data, 0, data.size).also { count = it } != -1) { outputStream.write(data, 0, count) } } } block(tempFile) } } } fun String.resolveBuild() = download { file -> ZipFile(file).use { zip -> zip.getEntry("android-studio/build.txt").let { entry -> zip.getInputStream(entry).use { inputStream -> inputStream.readBytes().toString(Charsets.UTF_8) }.also { println(" Resolved build number: $it") } } }.also { file.delete() } } fun String.toLooseVersion() = split('.').map { it.take(4).toInt() }.let { val (major, minor, patch) = it + 0 SemVer(major, minor, patch) } fun file(path: String) = File(System.getenv("GITHUB_WORKSPACE") ?: "../../").resolve(path).also(File::createNewFile) @Root(strict = false) data class Content( @field:Attribute var version: Int = 1, @field:ElementList(inline = true, required = false) var items: List = mutableListOf(), ) data class Item( var name: String = "", var build: String = "", var version: String = "", var channel: String = "", var platformBuild: String? = null, var platformVersion: String? = null, var date: String = "", )