Skip to content

Commit 0b060f9

Browse files
Add WASI timezones implementation
Co-authored-by: Dmitry Khalanskiy <[email protected]>
1 parent 2725652 commit 0b060f9

File tree

12 files changed

+332
-6
lines changed

12 files changed

+332
-6
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package TzdbTasks
7+
8+
import org.gradle.api.file.DirectoryProperty
9+
import org.gradle.api.provider.Property
10+
import org.gradle.api.tasks.*
11+
import java.io.File
12+
13+
abstract class InstallTimeTzdb : Exec() {
14+
@get:OutputDirectory
15+
@get:Optional
16+
abstract val outputDirectory: DirectoryProperty
17+
18+
@get:Input
19+
@get:Optional
20+
abstract val version: Property<String>
21+
22+
init {
23+
outputDirectory.convention(project.layout.buildDirectory.dir("time-tzdb"))
24+
executable = "npm"
25+
}
26+
27+
override fun exec() {
28+
val installVersion = version.orNull?.let { "@$it" } ?: ""
29+
this.setArgs(listOf("install", "--prefix", outputDirectory.get().asFile, "@tubular/time-tzdb$installVersion"))
30+
super.exec()
31+
}
32+
}
33+
34+
enum class ConvertType {
35+
LARGE, SMALL
36+
}
37+
38+
abstract class TzdbDownloadAndCompile : Exec() {
39+
@get:Input
40+
@get:Optional
41+
abstract val ianaVersion: Property<String>
42+
43+
@get:Input
44+
@get:Optional
45+
abstract val convertType: Property<ConvertType>
46+
47+
@get:OutputDirectory
48+
@get:Optional
49+
abstract val outputDirectory: DirectoryProperty
50+
51+
@get:InputDirectory
52+
abstract val timeTzdbDirectory: DirectoryProperty
53+
54+
init {
55+
val typePostfix = convertType.map {
56+
when(it) {
57+
ConvertType.LARGE -> "-large"
58+
ConvertType.SMALL -> "-small"
59+
}
60+
}
61+
62+
val outputDir = project.layout.buildDirectory.zip(typePostfix) { build, type -> build.dir("tzdbCompiled$type") }
63+
64+
outputDirectory.convention(outputDir)
65+
convertType.convention(ConvertType.LARGE)
66+
executable = ""
67+
}
68+
69+
override fun exec() {
70+
executable = File(timeTzdbDirectory.get().asFile, "node_modules/.bin/tzc").path
71+
val installVersion = ianaVersion.orNull
72+
val convertTypeArg = when(convertType.get()) {
73+
ConvertType.LARGE -> "--large"
74+
ConvertType.SMALL -> "--small"
75+
}
76+
if (installVersion.isNullOrEmpty()) {
77+
this.setArgs(listOf(outputDirectory.get().asFile, "-b", "-o", convertTypeArg))
78+
} else {
79+
this.setArgs(listOf(outputDirectory.get().asFile, "-b", "-o", convertTypeArg, "-u", installVersion))
80+
}
81+
super.exec()
82+
}
83+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import java.io.File
2+
3+
/*
4+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
5+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
6+
*/
7+
8+
private val pkg = "package kotlinx.datetime.timezones.tzData"
9+
10+
private fun generateByteArrayProperty(tzData: TzData, header: String, propertyName: String): String = buildString {
11+
append(header)
12+
appendLine()
13+
appendLine()
14+
appendLine("/* ${tzData.fullTzNames.joinToString(", ")} */")
15+
append("internal val $propertyName get() = byteArrayOf(")
16+
for (chunk in tzData.data.toList().chunked(16)) {
17+
appendLine()
18+
append(" ")
19+
val chunkText = chunk.joinToString {
20+
it.toString().padStart(4, ' ')
21+
} + ","
22+
append(chunkText)
23+
}
24+
appendLine()
25+
append(")")
26+
}
27+
28+
private class TzData(val data: ByteArray, val fullTzNames: MutableList<String>)
29+
private fun loadTzBinaries(
30+
zoneInfo: File,
31+
currentName: String,
32+
result: MutableList<TzData>
33+
) {
34+
val zoneName = if (currentName.isEmpty()) zoneInfo.name else "$currentName/${zoneInfo.name}"
35+
if (zoneInfo.isDirectory) {
36+
zoneInfo.listFiles()?.forEach {
37+
loadTzBinaries(it, zoneName, result)
38+
}
39+
} else {
40+
val bytes = zoneInfo.readBytes()
41+
val foundTzData = result.firstOrNull { it.data.contentEquals(bytes) }
42+
val tzData: TzData
43+
if (foundTzData != null) {
44+
tzData = foundTzData
45+
} else {
46+
tzData = TzData(bytes, mutableListOf())
47+
result.add(tzData)
48+
}
49+
50+
tzData.fullTzNames.add(zoneName)
51+
}
52+
}
53+
54+
fun generateZoneInfosResources(zoneInfoDir: File, outputDir: File, version: String) {
55+
val header = buildString {
56+
appendLine()
57+
append("/* AUTOGENERATED FROM ZONE INFO DATABASE v.$version */")
58+
appendLine()
59+
appendLine()
60+
append(pkg)
61+
}
62+
63+
val loadedZones = mutableListOf<TzData>()
64+
zoneInfoDir.listFiles()?.forEach { file ->
65+
loadTzBinaries(file, "", loadedZones)
66+
}
67+
68+
val zoneDataByNameBody = StringBuilder()
69+
val getTimeZonesBody = StringBuilder()
70+
loadedZones.forEachIndexed { id, tzData ->
71+
val tzDataName = "tzData$id"
72+
val data = generateByteArrayProperty(tzData, header, tzDataName)
73+
File(outputDir, "$tzDataName.kt").writeText(data)
74+
tzData.fullTzNames.forEach { name ->
75+
zoneDataByNameBody.appendLine(" \"$name\" -> $tzDataName")
76+
getTimeZonesBody.appendLine(" \"$name\",")
77+
}
78+
}
79+
80+
val content = buildString {
81+
append(header)
82+
appendLine()
83+
appendLine()
84+
appendLine("internal fun zoneDataByName(name: String): ByteArray = when(name) {")
85+
append(zoneDataByNameBody)
86+
appendLine()
87+
append(" else -> throw kotlinx.datetime.IllegalTimeZoneException(\"Invalid timezone name\")")
88+
appendLine()
89+
append("}")
90+
appendLine()
91+
appendLine()
92+
append("internal val timeZones: Set<String> by lazy { setOf(")
93+
appendLine()
94+
append(getTimeZonesBody)
95+
appendLine()
96+
append(")")
97+
append("}")
98+
}
99+
100+
File(outputDir, "tzData.kt").writeText(content)
101+
}

core/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ kotlin {
250250

251251
val wasmWasiTest by getting {
252252
dependsOn(pureKotlinTest)
253+
dependencies {
254+
runtimeOnly(project(":kotlinx-datetime-zoneinfo"))
255+
}
253256
}
254257

255258
val darwinMain by getting {

core/common/src/TimeZone.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ public expect open class TimeZone {
6363
*
6464
* If the current system time zone changes, this function can reflect this change on the next invocation.
6565
*
66+
* Always returns the `UTC` timezone on the Wasm WASI platform due to the lack of support for retrieving system timezone information.
67+
*
6668
* @sample kotlinx.datetime.test.samples.TimeZoneSamples.currentSystemDefault
6769
*/
6870
public fun currentSystemDefault(): TimeZone
@@ -92,6 +94,10 @@ public expect open class TimeZone {
9294
*
9395
* @throws IllegalTimeZoneException if [zoneId] has an invalid format or a time-zone with the name [zoneId]
9496
* is not found.
97+
*
98+
* @throws IllegalTimeZoneException on the Wasm WASI platform for non-fixed-offset time zones,
99+
* unless a dependency on the `kotlinx-datetime-zoneinfo` artifact is added.
100+
*
95101
* @sample kotlinx.datetime.test.samples.TimeZoneSamples.constructorFunction
96102
*/
97103
public fun of(zoneId: String): TimeZone

core/wasmWasi/src/internal/Platform.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ internal actual fun currentTime(): Instant = clockTimeGet().let { time ->
3939
}
4040

4141
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> =
42-
throw UnsupportedOperationException("WASI platform does not support system timezone obtaining")
42+
"UTC" to null
4343

4444
internal actual val systemTzdb: TimeZoneDatabase = TzdbOnData()

core/wasmWasi/src/internal/TimeZonesInitializer.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This is internal API which is not intended to use on user-side.
1515
*/
1616
@InternalDateTimeApi
1717
public interface TimeZonesProvider {
18-
public fun zoneDataByName(name: String): ByteArray
18+
public fun zoneDataByName(name: String): ByteArray
1919
public fun getTimeZones(): Set<String>
2020
}
2121

@@ -31,15 +31,14 @@ public fun initializeTimeZonesProvider(provider: TimeZonesProvider) {
3131
@InternalDateTimeApi
3232
private var timeZonesProvider: TimeZonesProvider? = null
3333

34+
@OptIn(InternalDateTimeApi::class)
3435
internal class TzdbOnData: TimeZoneDatabase {
3536
override fun rulesForId(id: String): TimeZoneRules {
36-
@OptIn(InternalDateTimeApi::class)
3737
val data = timeZonesProvider?.zoneDataByName(id)
3838
?: throw IllegalTimeZoneException("TimeZones are not supported")
3939
return readTzFile(data).toTimeZoneRules()
4040
}
4141

4242
override fun availableTimeZoneIds(): Set<String> =
43-
@OptIn(InternalDateTimeApi::class)
44-
timeZonesProvider?.getTimeZones() ?: emptySet()
43+
timeZonesProvider?.getTimeZones() ?: setOf("UTC")
4544
}

gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ group=org.jetbrains.kotlinx
55
version=0.6.0
66
versionSuffix=SNAPSHOT
77

8+
tzdbVersion=2024a
9+
810
defaultKotlinVersion=1.9.21
911
dokkaVersion=1.9.20
1012
serializationVersion=1.6.2

serialization/build.gradle.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ kotlin {
109109
}
110110

111111
val wasmWasiMain by getting
112-
val wasmWasiTest by getting
112+
val wasmWasiTest by getting {
113+
dependencies {
114+
runtimeOnly(project(":kotlinx-datetime-zoneinfo"))
115+
}
116+
}
113117

114118
val nativeMain by getting
115119
val nativeTest by getting

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ rootProject.name = "Kotlin-DateTime-library"
1616

1717
include(":core")
1818
project(":core").name = "kotlinx-datetime"
19+
include(":timezones/full")
20+
project(":timezones/full").name = "kotlinx-datetime-zoneinfo"
1921
include(":serialization")
2022
project(":serialization").name = "kotlinx-datetime-serialization"
2123
include(":benchmarks")

timezones/full/build.gradle.kts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
import TzdbTasks.ConvertType
7+
import TzdbTasks.InstallTimeTzdb
8+
import TzdbTasks.TzdbDownloadAndCompile
9+
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
10+
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolverPlugin
11+
import java.util.*
12+
13+
plugins {
14+
kotlin("multiplatform")
15+
id("maven-publish")
16+
}
17+
18+
val tzdbVersion: String by rootProject.properties
19+
version = "$tzdbVersion-spi.$version"
20+
21+
val convertedKtFilesDir = File(project.buildDir, "wasmWasi-full/src/internal/tzData")
22+
val tzdbDirectory = File(project.projectDir, "tzdb")
23+
24+
kotlin {
25+
@OptIn(ExperimentalWasmDsl::class)
26+
wasmWasi {
27+
nodejs()
28+
NpmResolverPlugin.apply(project) //Workaround KT-66373
29+
}
30+
31+
sourceSets.all {
32+
val suffixIndex = name.indexOfLast { it.isUpperCase() }
33+
val targetName = name.substring(0, suffixIndex)
34+
val suffix = name.substring(suffixIndex).toLowerCase(Locale.ROOT).takeIf { it != "main" }
35+
kotlin.srcDir("$targetName/${suffix ?: "src"}")
36+
resources.srcDir("$targetName/${suffix?.let { it + "Resources" } ?: "resources"}")
37+
}
38+
39+
sourceSets {
40+
commonMain {
41+
dependencies {
42+
compileOnly(project(":kotlinx-datetime"))
43+
}
44+
}
45+
46+
val wasmWasiMain by getting {
47+
kotlin.srcDir(convertedKtFilesDir)
48+
languageSettings.optIn("kotlinx.datetime.internal.InternalDateTimeApi")
49+
}
50+
51+
val wasmWasiTest by getting {
52+
dependencies {
53+
runtimeOnly(project(":kotlinx-datetime"))
54+
implementation(kotlin("test"))
55+
}
56+
}
57+
}
58+
}
59+
60+
val timeTzdbInstall by tasks.creating(InstallTimeTzdb::class) { }
61+
62+
val tzdbDownloadAndCompile by tasks.creating(TzdbDownloadAndCompile::class) {
63+
dependsOn(timeTzdbInstall)
64+
timeTzdbDirectory.set(timeTzdbInstall.outputDirectory)
65+
outputDirectory.set(tzdbDirectory)
66+
ianaVersion.set(tzdbVersion)
67+
convertType.set(ConvertType.LARGE)
68+
}
69+
70+
val generateWasmWasiZoneInfo by tasks.registering {
71+
inputs.dir(tzdbDirectory)
72+
outputs.dir(convertedKtFilesDir)
73+
doLast {
74+
generateZoneInfosResources(tzdbDirectory, convertedKtFilesDir, tzdbVersion)
75+
}
76+
}
77+
78+
tasks.getByName("compileKotlinWasmWasi") {
79+
dependsOn(generateWasmWasiZoneInfo)
80+
}
81+
82+
tasks.getByName("wasmWasiSourcesJar") {
83+
dependsOn(generateWasmWasiZoneInfo)
84+
}

0 commit comments

Comments
 (0)