Skip to content

Implement the timezone database for Linux and Windows in Kotlin #286

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

Merged
merged 2 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
[submodule "date-cpp-library/date"]
path = thirdparty/date
url = https://github.com/HowardHinnant/date
9 changes: 0 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,15 +381,6 @@ Add a dependency to the `<dependencies>` element. Note that you need to use the

## Building

Before building, ensure that you have [thirdparty/date](thirdparty/date) submodule initialized and updated.
IDEA does that automatically when cloning the repository, and if you cloned it in the command line, you may need
to run additionally:

```kotlin
git submodule init
git submodule update
```

The project requires JDK 8 to build classes and to run tests.
Gradle will try to find it among the installed JDKs or [provision](https://docs.gradle.org/current/userguide/toolchains.html#sec:provisioning) it automatically if it couldn't be found.
The path to JDK 8 can be additionally specified with the environment variable `JDK_8`.
Expand Down
156 changes: 65 additions & 91 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,41 +37,45 @@ kotlin {
explicitApi()

infra {
// Tiers are in accordance with <https://kotlinlang.org/docs/native-target-support.html>
// Tier 1
target("linuxX64")
// Tier 2
target("linuxArm64")
common("nix") {
// Tiers are in accordance with <https://kotlinlang.org/docs/native-target-support.html>
common("linux") {
// Tier 1
target("linuxX64")
// Tier 2
target("linuxArm64")
// Tier 4 (deprecated, but still in demand)
target("linuxArm32Hfp")
}
// the following targets are not supported, as we don't have timezone database implementations for them:
/*
target("androidNativeArm32")
target("androidNativeArm64")
target("androidNativeX86")
target("androidNativeX64")
*/
common("darwin") {
// Tier 1
target("macosX64")
target("macosArm64")
target("iosSimulatorArm64")
target("iosX64")
// Tier 2
target("watchosSimulatorArm64")
target("watchosX64")
target("watchosArm32")
target("watchosArm64")
target("tvosSimulatorArm64")
target("tvosX64")
target("tvosArm64")
target("iosArm64")
// Tier 3
target("watchosDeviceArm64")
}
}
// Tier 3
target("mingwX64")
// the following targets are not supported by kotlinx.serialization:
/*
target("androidNativeArm32")
target("androidNativeArm64")
target("androidNativeX86")
target("androidNativeX64")
*/
// Tier 4 (deprecated, but still in demand)
target("linuxArm32Hfp")

// Darwin targets are listed separately
common("darwin") {
// Tier 1
target("macosX64")
target("macosArm64")
target("iosSimulatorArm64")
target("iosX64")
// Tier 2
target("watchosSimulatorArm64")
target("watchosX64")
target("watchosArm32")
target("watchosArm64")
target("tvosSimulatorArm64")
target("tvosX64")
target("tvosArm64")
target("iosArm64")
// Tier 3
target("watchosDeviceArm64")
common("windows") {
target("mingwX64")
}
}

Expand Down Expand Up @@ -138,50 +142,26 @@ kotlin {
compilations["test"].kotlinOptions {
freeCompilerArgs += listOf("-trw")
}
if (konanTarget.family.isAppleFamily) {
return@withType
}
compilations["main"].cinterops {
create("date") {
val cinteropDir = "$projectDir/native/cinterop"
val dateLibDir = "${project(":").projectDir}/thirdparty/date"
headers("$cinteropDir/public/cdate.h")
defFile("native/cinterop/date.def")
extraOpts("-Xsource-compiler-option", "-I$cinteropDir/public")
extraOpts("-Xsource-compiler-option", "-DONLY_C_LOCALE=1")
when {
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX -> {
// needed for the date library so that it does not try to download the timezone database
extraOpts("-Xsource-compiler-option", "-DUSE_OS_TZDB=1")
/* using a more modern C++ version causes the date library to use features that are not
* present in the currently outdated GCC root shipped with Kotlin/Native for Linux. */
extraOpts("-Xsource-compiler-option", "-std=c++11")
// the date library and its headers
extraOpts("-Xcompile-source", "$dateLibDir/src/tz.cpp")
extraOpts("-Xsource-compiler-option", "-I$dateLibDir/include")
// the main source for the platform bindings.
extraOpts("-Xcompile-source", "$cinteropDir/cpp/cdate.cpp")
}
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW -> {
// needed to be able to use std::shared_mutex to implement caching.
extraOpts("-Xsource-compiler-option", "-std=c++17")
// the date library headers, needed for some pure calculations.
extraOpts("-Xsource-compiler-option", "-I$dateLibDir/include")
// the main source for the platform bindings.
extraOpts("-Xcompile-source", "$cinteropDir/cpp/windows.cpp")
}
else -> {
throw IllegalArgumentException("Unknown native target ${this@withType}")
when {
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW -> {
compilations["main"].cinterops {
create("declarations") {
defFile("$projectDir/windows/cinterop/definitions.def")
headers("$projectDir/windows/cinterop/definitions.h")
}
}
}
}
compilations["main"].defaultSourceSet {
kotlin.srcDir("native/cinterop_actuals")
konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX -> {
// do nothing special
}
konanTarget.family.isAppleFamily -> {
// do nothing special
}
else -> {
throw IllegalArgumentException("Unknown native target ${this@withType}")
}
}
}


sourceSets {
commonMain {
dependencies {
Expand Down Expand Up @@ -328,10 +308,10 @@ tasks {

val downloadWindowsZonesMapping by tasks.registering {
description = "Updates the mapping between Windows-specific and usual names for timezones"
val output = "$projectDir/native/cinterop/public/windows_zones.hpp"
val initialFileContents = File(output).readBytes()
val output = "$projectDir/windows/src/WindowsZoneNames.kt"
outputs.file(output)
doLast {
val initialFileContents = try { File(output).readBytes() } catch(e: Throwable) { ByteArray(0) }
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
// otherwise, parsing fails since it can't find the dtd
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
Expand All @@ -343,11 +323,13 @@ val downloadWindowsZonesMapping by tasks.registering {
xmlDoc.documentElement.normalize()
val mapZones = xmlDoc.getElementsByTagName("mapZone")
val mapping = linkedMapOf<String, String>()
mapping["UTC"] = "UTC"
for (i in 0 until mapZones.length) {
val mapZone = mapZones.item(i)
val windowsName = mapZone.attributes.getNamedItem("other").nodeValue
val usualNames = mapZone.attributes.getNamedItem("type").nodeValue
for (usualName in usualNames.split(' ')) {
if (usualName == "") continue
val oldWindowsName = mapping[usualName] // don't do it in `put` to preserve the order in the map
if (oldWindowsName == null) {
mapping[usualName] = windowsName
Expand All @@ -360,31 +342,23 @@ val downloadWindowsZonesMapping by tasks.registering {
val bos = ByteArrayOutputStream()
PrintWriter(bos).use { out ->
out.println("""// generated with gradle task `$name`""")
out.println("""#include <unordered_map>""")
out.println("""#include <string>""")
out.println("""static const std::unordered_map<std::string, std::string> standard_to_windows = {""")
out.println("""package kotlinx.datetime""")
out.println("""internal val standardToWindows: Map<String, String> = mutableMapOf(""")
for ((usualName, windowsName) in sortedMapping) {
out.println("\t{ \"$usualName\", \"$windowsName\" },")
out.println(" \"$usualName\" to \"$windowsName\",")
}
out.println("};")
out.println("""static const std::unordered_map<std::string, std::string> windows_to_standard = {""")
out.println(")")
out.println("""internal val windowsToStandard: Map<String, String> = mutableMapOf(""")
val reverseMap = sortedMapOf<String, String>()
for ((usualName, windowsName) in mapping) {
if (reverseMap[windowsName] == null) {
reverseMap[windowsName] = usualName
}
}
for ((windowsName, usualName) in reverseMap) {
out.println("\t{ \"$windowsName\", \"$usualName\" },")
out.println(" \"$windowsName\" to \"$usualName\",")
}
out.println("};")
out.println("""static const std::unordered_map<std::string, size_t> zone_ids = {""")
var i = 0
for ((usualName, windowsName) in sortedMapping) {
out.println("\t{ \"$usualName\", $i },")
++i
}
out.println("};")
out.println(")")
}
val newFileContents = bos.toByteArray()
if (!(initialFileContents contentEquals newFileContents)) {
Expand Down Expand Up @@ -422,4 +396,4 @@ tasks.configureEach {
with(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.apply(rootProject)) {
nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2"
nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"
}
}
51 changes: 51 additions & 0 deletions core/common/src/internal/BinaryDataReader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime.internal

/**
* A helper for reading binary data.
*/
internal class BinaryDataReader(private val bytes: ByteArray, private var position: Int = 0) {
/**
* Reads a byte.
*/
fun readByte(): Byte = bytes[position++]

/**
* Reads an unsigned byte.
*/
fun readUnsignedByte(): UByte =
readByte().toUByte()

/**
* Reads a big-endian (network byte order) 32-bit integer.
*/
fun readInt(): Int =
(bytes[position].toInt() and 0xFF shl 24) or
(bytes[position + 1].toInt() and 0xFF shl 16) or
(bytes[position + 2].toInt() and 0xFF shl 8) or
(bytes[position + 3].toInt() and 0xFF).also { position += 4 }

/**
* Reads a big-endian (network byte order) 64-bit integer.
*/
fun readLong(): Long =
(bytes[position].toLong() and 0xFF shl 56) or
(bytes[position + 1].toLong() and 0xFF shl 48) or
(bytes[position + 2].toLong() and 0xFF shl 40) or
(bytes[position + 3].toLong() and 0xFF shl 32) or
(bytes[position + 4].toLong() and 0xFF shl 24) or
(bytes[position + 5].toLong() and 0xFF shl 16) or
(bytes[position + 6].toLong() and 0xFF shl 8) or
(bytes[position + 7].toLong() and 0xFF).also { position += 8 }

fun readUtf8String(length: Int) =
bytes.decodeToString(position, position + length).also { position += length }

fun readAsciiChar(): Char = readByte().toInt().toChar()

fun skip(length: Int) { position += length }
}
Loading