diff --git a/.github/workflows/release-js.yml b/.github/workflows/release-js.yml index 8e3ad38e02..844c73829b 100644 --- a/.github/workflows/release-js.yml +++ b/.github/workflows/release-js.yml @@ -32,4 +32,4 @@ jobs: env: SONATYPE_USERNAME: '${{ secrets.SONATYPE_USER }}' SONATYPE_PASSWORD: '${{ secrets.SONATYPE_PASS }}' - run: SCALAJS=true ./sbt sonatypeBundleRelease + run: SCALA_JS=true ./sbt sonatypeBundleRelease diff --git a/.github/workflows/release-native.yml b/.github/workflows/release-native.yml new file mode 100644 index 0000000000..a19ec493be --- /dev/null +++ b/.github/workflows/release-native.yml @@ -0,0 +1,35 @@ +name: Release Scala Native + +on: + push: + tags: + - v* + workflow_dispatch: + +jobs: + publish_js: + name: Publish Scala Native + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # Fetch all tags so that sbt-dynver can find the previous release version + fetch-depth: 0 + - run: git fetch --tags -f + - uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + - name: Setup GPG + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + run: echo $PGP_SECRET | base64 --decode | gpg --import --batch --yes + - name: Build for Scala Native + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: ./sbt publishNativeSigned + - name: Release to Sonatype + env: + SONATYPE_USERNAME: '${{ secrets.SONATYPE_USER }}' + SONATYPE_PASSWORD: '${{ secrets.SONATYPE_PASS }}' + run: SCALA_NATIVE=true ./sbt sonatypeBundleRelease diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce37f80f68..0451d54a41 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -212,6 +212,25 @@ jobs: check_name: Test Report Scala.js / Scala 3 annotate_only: true detailed_summary: true + test_native_3: + name: Scala Native / Scala 3 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '21' + - name: Scala Native test + run: JVM_OPTS=-Xmx4g ./sbt "++ 3; projectNative/test" + - name: Publish Test Report + uses: mikepenz/action-junit-report@v4 + if: always() # always run even if the previous step fails + with: + report_paths: '**/target/test-reports/TEST-*.xml' + check_name: Test Report Scala Native / Scala 3 + annotate_only: true + detailed_summary: true test_airspec: name: AirSpec runs-on: ubuntu-latest diff --git a/airframe-log/.native/src/main/scala-3/java/util/logging/Level.scala b/airframe-log/.native/src/main/scala-3/java/util/logging/Level.scala deleted file mode 100644 index 61ea700db9..0000000000 --- a/airframe-log/.native/src/main/scala-3/java/util/logging/Level.scala +++ /dev/null @@ -1,20 +0,0 @@ -package java.util.logging - - -case class Level(name: String, value: Int) extends Ordered[Level] { - override def compare(other: Level): Int = value.compare(other.value) - def intValue(): Int = value - override def toString: String = name -} - -object Level: - val OFF = Level("OFF", 0) - val SEVERE = Level("SEVERE", 1000) - val WARNING = Level("WARNING", 900) - val INFO = Level("INFO", 800) - val CONFIG = Level("CONFIG", 700) - val FINE = Level("FINE", 500) - val FINER = Level("FINER", 400) - val FINEST = Level("FINEST", 300) - val ALL = Level("ALL", Integer.MIN_VALUE) - diff --git a/airframe-log/.native/src/main/scala/java/util/logging/Level.scala b/airframe-log/.native/src/main/scala/java/util/logging/Level.scala new file mode 100644 index 0000000000..4cbd80e2e5 --- /dev/null +++ b/airframe-log/.native/src/main/scala/java/util/logging/Level.scala @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package java.util.logging + +case class Level(name: String, value: Int) extends Ordered[Level] { + override def compare(other: Level): Int = value.compare(other.value) + def intValue(): Int = value + override def toString: String = name +} + +object Level { + val OFF = Level("OFF", 0) + val SEVERE = Level("SEVERE", 1000) + val WARNING = Level("WARNING", 900) + val INFO = Level("INFO", 800) + val CONFIG = Level("CONFIG", 700) + val FINE = Level("FINE", 500) + val FINER = Level("FINER", 400) + val FINEST = Level("FINEST", 300) + val ALL = Level("ALL", Integer.MIN_VALUE) +} diff --git a/airframe-log/.native/src/main/scala-3/java/util/logging/Logger.scala b/airframe-log/.native/src/main/scala/java/util/logging/Logger.scala similarity index 50% rename from airframe-log/.native/src/main/scala-3/java/util/logging/Logger.scala rename to airframe-log/.native/src/main/scala/java/util/logging/Logger.scala index 5aac70e365..be91a23f00 100644 --- a/airframe-log/.native/src/main/scala-3/java/util/logging/Logger.scala +++ b/airframe-log/.native/src/main/scala/java/util/logging/Logger.scala @@ -1,78 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package java.util.logging - -abstract class Handler extends AutoCloseable: +abstract class Handler extends AutoCloseable { def publish(record: LogRecord): Unit + def flush(): Unit +} /** - * Implements java.util.logging.Logger interface, which is not avaialble - * in Scala Native - * @param name - */ + * Implements java.util.logging.Logger interface, which is not avaialble in Scala Native + * @param name + */ class Logger(parent: Option[Logger], name: String) { - private var handlers = List.empty[Handler] - private var useParentHandlers = true + private var handlers = List.empty[Handler] + private var useParentHandlers = true private var level: Option[Level] = None def getName(): String = name - def log(level: Level, msg: String): Unit = { + def log(level: Level, msg: String): Unit = log(LogRecord(level, msg)) - } def log(record: LogRecord): Unit = { - if(isLoggable(record.getLevel())) { - if(record.getLoggerName() == null) { - record.setLoggerName(name) - } - if(parent.nonEmpty && useParentHandlers) then + if (isLoggable(record.getLevel())) { + if (record.getLoggerName() == null) record.setLoggerName(name) + if (parent.nonEmpty && useParentHandlers) { getParent().log(record) - else + } else { handlers.foreach { h => h.publish(record) } + } } } def isLoggable(level: Level): Boolean = { val l = getLevel() - if(level.intValue() < l.intValue()) then false else true + level.intValue() >= l.intValue() } - def getParent(): Logger = { + def getParent(): Logger = parent.getOrElse(null) - } - def getLevel(): Level = { + def getLevel(): Level = level.orElse(parent.map(_.getLevel())).getOrElse(Level.INFO) - } - def setLevel(newLevel: Level): Unit = { - level = Some(newLevel) - } + def setLevel(newLevel: Level): Unit = + level = Option(newLevel) - def resetLogLevel(): Unit = { + def resetLogLevel(): Unit = level = None - } - def setUseParentHandlers(useParentHandlers: Boolean): Unit = { + def setUseParentHandlers(useParentHandlers: Boolean): Unit = this.useParentHandlers = useParentHandlers - } - def addHandler(h: Handler): Unit = { + def addHandler(h: Handler): Unit = handlers = h :: handlers - } - def removeHandler(h: Handler): Unit = { + def removeHandler(h: Handler): Unit = handlers = handlers.filter(_ != h) - } def getHandlers: Array[Handler] = handlers.toArray } -object Logger: +object Logger { + import scala.jdk.CollectionConverters.* + private val loggerTable = new java.util.concurrent.ConcurrentHashMap[String, Logger]().asScala - private val rootLogger = Logger(None, "") + private val rootLogger = Logger(None, "") def getLogger(name: String): Logger = { loggerTable.get(name) match { @@ -90,31 +96,35 @@ object Logger: name match { case null | "" => rootLogger case other => - val parentName = name.substring(0, name.lastIndexOf('.').max(0)) + val parentName = name.substring(0, name.lastIndexOf('.').max(0)) val parentLogger = getLogger(parentName) Logger(Some(parentLogger), name) } } +} - -abstract class Formatter: +abstract class Formatter { def format(record: LogRecord): String +} - -class LogRecord(_level: Level, msg: String) extends Serializable: - private val millis = System.currentTimeMillis() - private var loggerName = "" +class LogRecord(_level: Level, msg: String) extends Serializable { + private val millis = System.currentTimeMillis() + private var loggerName = "" private var thrown: Throwable = null def getMessage(): String = msg + def getMillis(): Long = millis + def getLoggerName(): String = loggerName + def getLevel(): Level = _level + def getThrown(): Throwable = thrown - def setLoggerName(name: String): Unit = { + def setLoggerName(name: String): Unit = this.loggerName = name - } - def setThrown(e: Throwable): Unit = { + + def setThrown(e: Throwable): Unit = thrown = e - } +} diff --git a/airframe-log/.native/src/main/scala-3/wvlet/log/LogEnv.scala b/airframe-log/.native/src/main/scala/wvlet/log/LogEnv.scala similarity index 88% rename from airframe-log/.native/src/main/scala-3/wvlet/log/LogEnv.scala rename to airframe-log/.native/src/main/scala/wvlet/log/LogEnv.scala index 2b9548982d..75af34c6ac 100644 --- a/airframe-log/.native/src/main/scala-3/wvlet/log/LogEnv.scala +++ b/airframe-log/.native/src/main/scala/wvlet/log/LogEnv.scala @@ -31,8 +31,16 @@ private[log] object LogEnv extends LogEnvBase { * @param cl * @return */ - override def getLoggerName(cl: Class[_]): String = cl.getName + override def getLoggerName(cl: Class[?]): String = { + var name = cl.getName + val pos = name.indexOf("$") + if (pos > 0) { + // Remove trailing $xxx + name = name.substring(0, pos) + } + name + } override def scheduleLogLevelScan: Unit = {} override def stopScheduledLogLevelScan: Unit = {} @@ -53,5 +61,4 @@ private[log] object LogEnv extends LogEnvBase { /** */ override def unregisterJMX: Unit = {} - } diff --git a/airframe-log/.native/src/main/scala-3/wvlet/log/LogTimestampFormatter.scala b/airframe-log/.native/src/main/scala/wvlet/log/LogTimestampFormatter.scala similarity index 57% rename from airframe-log/.native/src/main/scala-3/wvlet/log/LogTimestampFormatter.scala rename to airframe-log/.native/src/main/scala/wvlet/log/LogTimestampFormatter.scala index d3f060deef..7b7904a920 100644 --- a/airframe-log/.native/src/main/scala-3/wvlet/log/LogTimestampFormatter.scala +++ b/airframe-log/.native/src/main/scala/wvlet/log/LogTimestampFormatter.scala @@ -1,3 +1,16 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package wvlet.log import scalanative.posix.time.* @@ -17,7 +30,7 @@ object LogTimestampFormatter { !ttPtr = (timeMillis / 1000).toSize val tmPtr = alloc[tm]() localtime_r(ttPtr, tmPtr) - val bufSize = 26.toUSize + val bufSize = 26.toUSize val buf: Ptr[Byte] = alloc[Byte](bufSize) strftime(buf, bufSize, pattern, tmPtr) val ms = timeMillis % 1000 @@ -33,11 +46,9 @@ object LogTimestampFormatter { } } - def formatTimestamp(timeMillis: Long): String = { + def formatTimestamp(timeMillis: Long): String = format(c"%Y-%m-%d %H:%M:%S.", timeMillis) - } - def formatTimestampWithNoSpaace(timeMillis: Long): String = { + def formatTimestampWithNoSpaace(timeMillis: Long): String = format(c"%Y-%m-%dT%H:%M:%S.", timeMillis) - } } diff --git a/airframe-log/src/test/scala/wvlet/log/LoggerTest.scala b/airframe-log/src/test/scala/wvlet/log/LoggerTest.scala index 2e92e63bba..7ee1f8d01f 100644 --- a/airframe-log/src/test/scala/wvlet/log/LoggerTest.scala +++ b/airframe-log/src/test/scala/wvlet/log/LoggerTest.scala @@ -186,19 +186,19 @@ class LoggerTest extends Spec { } test("use succinct name when used with anonymous trait") { - if (LogEnv.isScalaJS) { - pending("Scala.js cannot get a logger name") + if (isScalaJS || isScalaNative) { + pending("Scala.js/Native cannot get a logger name from anonymous trait") } else { val l = new Sample with LogSupport { self => - assert(self.logger.getName == ("wvlet.log.Sample")) + self.logger.getName shouldBe "wvlet.log.Sample" } } } test("Remove $ from object name") { val o = Sample - assert(o.loggerName == "wvlet.log.Sample") + o.loggerName shouldBe "wvlet.log.Sample" } test("clear parent handlers") { diff --git a/build.sbt b/build.sbt index 1e269f0271..f06e7b3336 100644 --- a/build.sbt +++ b/build.sbt @@ -11,7 +11,7 @@ val targetScalaVersions = SCALA_3 :: uptoScala2 ThisBuild / resolvers ++= Resolver.sonatypeOssRepos("snapshots") val AIRSPEC_VERSION = sys.env.getOrElse("AIRSPEC_VERSION", "24.4.1") -val SCALACHECK_VERSION = "1.17.1" +val SCALACHECK_VERSION = "1.18.0" val MSGPACK_VERSION = "0.9.8" val SCALA_PARSER_COMBINATOR_VERSION = "2.4.0" val SQLITE_JDBC_VERSION = "3.45.3.0" @@ -32,13 +32,13 @@ val AIRFRAME_BINARY_COMPAT_VERSION = "23.6.0" // A short cut for publishing snapshots to Sonatype addCommandAlias( "publishSnapshots", - s"+ projectJVM/publish; + projectJS/publish" + s"+ projectJVM/publish; + projectJS/publish; + projectNative/publish" ) // [Development purpose] publish all artifacts to the local repo addCommandAlias( "publishAllLocal", - s"+ projectJVM/publishLocal; + projectJS/publishLocal;" + s"+ projectJVM/publishLocal; + projectJS/publishLocal; + projectNative/publishLocal" ) // [Development purpose] publish all sbt-airframe related artifacts to local repo @@ -55,6 +55,10 @@ addCommandAlias( "publishJSLocal", s"+ projectJS/publishLocal" ) +addCommandAlias( + "publishNativeSigned", + s"+ projectNative/publishSigned" +) // Allow using Ctrl+C in sbt without exiting the prompt // Global / cancelable := true @@ -162,6 +166,12 @@ val jsBuildSettings = Seq[Setting[?]]( coverageEnabled := false ) +val nativeBuildSettings = Seq[Setting[?]]( + scalaVersion := SCALA_3, + crossScalaVersions := List(SCALA_3), + coverageEnabled := false +) + val noPublish = Seq( publishArtifact := false, publish := {}, @@ -187,9 +197,11 @@ lazy val root = .settings( sonatypeProfileName := "org.wvlet", sonatypeSessionName := { - if (sys.env.isDefinedAt("SCALAJS")) { - // Use a different session for Scala.js projects + // Use different session names for parallel publishing to Sonatype + if (sys.env.isDefinedAt("SCALA_JS")) { s"${sonatypeSessionName.value} for Scala.js" + } else if (sys.env.isDefinedAt("SCALA_NATIVE")) { + s"${sonatypeSessionName.value} for Scala Native" } else { sonatypeSessionName.value } @@ -252,6 +264,10 @@ lazy val jsProjects: Seq[ProjectReference] = Seq( widgetJS ) +lazy val nativeProjects: Seq[ProjectReference] = Seq( + log.native +) + // Integration test projects lazy val itProjects: Seq[ProjectReference] = Seq( integrationTestApi.jvm, @@ -291,6 +307,15 @@ lazy val projectJS = ) .aggregate(jsProjects: _*) +lazy val projectNative = + project + .settings(noPublish) + .settings( + // Skip importing aggregated projects in IntelliJ IDEA + ideSkipProject := true + ) + .aggregate(nativeProjects: _*) + lazy val projectIt = project .settings(noPublish) @@ -559,7 +584,7 @@ val logJVMDependencies = Seq( // airframe-log should have minimum dependencies lazy val log: sbtcrossproject.CrossProject = - crossProject(JVMPlatform, JSPlatform) + crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file("airframe-log")) .settings(buildSettings) @@ -582,6 +607,9 @@ lazy val log: sbtcrossproject.CrossProject = ("org.scala-js" %%% "scalajs-java-logging" % JS_JAVA_LOGGING_VERSION).cross(CrossVersion.for3Use2_13) ) ) + .nativeSettings( + nativeBuildSettings + ) lazy val metrics = crossProject(JVMPlatform, JSPlatform) diff --git a/project/plugin.sbt b/project/plugin.sbt index 58e6dc146d..75654a91f0 100755 --- a/project/plugin.sbt +++ b/project/plugin.sbt @@ -32,7 +32,7 @@ libraryDependencies += "io.github.gmkumar2005" %% "scala-js-env-playwright" % "0 // For Scala native addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.1") // For setting explicit versions for each commit addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.0.1")