From 394a215635bc63e15ccdcb9062a20984951c16d3 Mon Sep 17 00:00:00 2001 From: Roman Langolf Date: Sun, 24 May 2026 13:28:36 +0700 Subject: [PATCH 1/6] try mill --- .devcontainer/devcontainer.json | 7 - .github/workflows/ci.yml | 4 +- .mill-version | 1 + README.md | 2 +- build.mill | 237 ++++++++++++++++++ mill | 199 +++++++++++++++ .../src/main/scala/{cli.scala => main.scala} | 4 +- .../{shared => }/src/main/scala/codegen.scala | 0 .../{shared => }/src/main/scala/json.scala | 0 project/build.properties | 1 - build.sbt => sbt_backup/build_sbt.bak | 54 ++-- sbt_backup/project/build_properties.bak | 1 + .../project/plugins_sbt.bak | 4 +- 13 files changed, 470 insertions(+), 44 deletions(-) delete mode 100644 .devcontainer/devcontainer.json create mode 100644 .mill-version create mode 100644 build.mill create mode 100755 mill rename modules/cli/src/main/scala/{cli.scala => main.scala} (97%) rename modules/core/{shared => }/src/main/scala/codegen.scala (100%) rename modules/example-jsoniter-json/{shared => }/src/main/scala/json.scala (100%) delete mode 100644 project/build.properties rename build.sbt => sbt_backup/build_sbt.bak (85%) create mode 100644 sbt_backup/project/build_properties.bak rename project/plugins.sbt => sbt_backup/project/plugins_sbt.bak (54%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 5c6a502..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "image": "mcr.microsoft.com/devcontainers/universal:2", - "features": { - "ghcr.io/devcontainers-extra/features/sbt-sdkman:2": {} - }, - "postCreateCommand": "sdk install sbt" -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9208c3f..e9e77f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - uses: coursier/cache-action@v6 - uses: VirtusLab/scala-cli-setup@main with: - jvm: temurin:21 + jvm: temurin:25 apps: sbt power: true - name: Start up emulators @@ -31,7 +31,7 @@ jobs: - uses: coursier/cache-action@v6 - uses: VirtusLab/scala-cli-setup@main with: - jvm: temurin:21 + jvm: temurin:25 apps: sbt - name: Import signing key if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' diff --git a/.mill-version b/.mill-version new file mode 100644 index 0000000..0664a8f --- /dev/null +++ b/.mill-version @@ -0,0 +1 @@ +1.1.6 diff --git a/README.md b/README.md index 0487752..2bf491b 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ See output in `example/out`. ##### Jsoniter Json type and codec example Jsoniter doesn't ship with a type that can represent raw Json values to be used for mapping of `any` / `object` types, but it provides methods to read / write raw values as bytes (related [issue](https://github.com/plokhotnyuk/jsoniter-scala/issues/1257)). -Given that we can create a custom type with a codec which can look for example like [that](modules/example-jsoniter-json/shared/src/main/scala/json.scala): +Given that we can create a custom type with a codec which can look for example like [that](modules/example-jsoniter-json/src/main/scala/json.scala): ```scala package example.jsoniter import com.github.plokhotnyuk.jsoniter_scala.core.* diff --git a/build.mill b/build.mill new file mode 100644 index 0000000..eb99027 --- /dev/null +++ b/build.mill @@ -0,0 +1,237 @@ +package build + +import mill.* +import mill.scalalib.* +import mill.scalanativelib.* +import mill.scalanativelib.api.* +import mill.scalalib.publish.* +import mill.util.VcsVersionModule +import mill.api.Task.Simple +import os.Path + +val scalaVer = "3.8.3" + +val scalaNativeVer = "0.5.11" + +val sttpClient4Ver = "4.0.23" + +val zioVer = "2.1.26" + +val zioJsonVer = "0.9.1" + +val jsoniterVer = "2.38.11" + +val munitVer = "1.3.0" + +val upickleVer = "4.4.3" + +def cliBinPath: os.Path = { + def normalise(s: String) = s.toLowerCase.replaceAll("[^a-z0-9]+", "") + val props = sys.props.toMap + val osName = normalise(props.getOrElse("os.name", "")) match { + case p if p.startsWith("linux") => "linux" + case p if p.startsWith("windows") => "windows" + case p if p.startsWith("osx") || p.startsWith("macosx") => "macosx" + case _ => "unknown" + } + val arch = ( + normalise(props.getOrElse("os.arch", "")), + props.getOrElse("sun.arch.data.model", "64") + ) match { + case ("amd64" | "x64" | "x8664" | "x86", bits) => s"x86_$bits" + case ("aarch64" | "arm64", bits) => s"aarch$bits" + case _ => "unknown" + } + os.pwd / "target" / "bin" / s"gcp-codegen-$arch-$osName" +} + +/** Build the Scala Native CLI binary and copy it to target/bin/- */ +def buildCliBinary: T[PathRef] = Task { + val linked = cli.nativeLink() + val dest = cliBinPath + os.makeDir.all(dest / os.up) + os.copy.over(linked.path, dest) + PathRef(dest) +} + +trait Publishable extends PublishModule with VcsVersionModule { + def pomSettings = PomSettings( + description = "Google Cloud client code generator", + organization = "dev.rolang", + url = "https://github.com/rolang/google-rest-api-codegen", + licenses = Seq(License.MIT), + versionControl = VersionControl.github("rolang", "google-rest-api-codegen"), + developers = Seq( + Developer(id = "rolang", name = "Roman Langolf", url = "https://rolang.dev", email = "rolang@pm.me") + ) + ) +} + +// --------------------------------------------------------------------------- +// core: published as gcp-codegen, JVM + Scala Native +// --------------------------------------------------------------------------- + +object core extends Module { + // jvm and native share src via SbtModule's parent-dir convention: + // /jvm/../src/main/scala == /src/main/scala + + trait CoreModule extends ScalaModule with SbtModule with Publishable { + override def moduleDir: Path = super.moduleDir / ".." / ".." / "modules" / "core" + + def scalaVersion = scalaVer + override def artifactName = Task { "gcp-codegen" } + override def mvnDeps = Task { Seq(mvn"com.lihaoyi::upickle::$upickleVer") } + } + + object jvm extends CoreModule + + object native extends CoreModule with ScalaNativeModule { + def scalaNativeVersion = scalaNativeVer + override def nativeMultithreading: T[Option[Boolean]] = Task { Some(false) } + } +} + +// --------------------------------------------------------------------------- +// cli: published as gcp-codegen-cli, Scala Native binary +// --------------------------------------------------------------------------- + +object cli extends ScalaNativeModule with Publishable with SbtModule { + override def moduleDir: Path = super.moduleDir / ".." / "modules" / "cli" + def scalaVersion = scalaVer + def scalaNativeVersion = scalaNativeVer + override def moduleDeps = Seq(core.native) + override def artifactName = Task { "gcp-codegen-cli" } + override def nativeMultithreading: T[Option[Boolean]] = Task { Some(false) } + override def releaseMode: T[ReleaseMode] = Task { ReleaseMode.ReleaseFast } +} + +// --------------------------------------------------------------------------- +// example-jsoniter-json: not published, JVM + Scala Native +// --------------------------------------------------------------------------- + +object exampleJsoniterJson extends Module { + trait ExampleModule extends ScalaModule with SbtModule { + def moduleDir = moduleDir / ".." / ".." / "modules" / "example-jsoniter-json" + + def scalaVersion = scalaVer + override def mvnDeps = Task { Seq(mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:$jsoniterVer") } + override def compileMvnDeps = Task { + Seq(mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:$jsoniterVer") + } + } + + object jvm extends ExampleModule + + object native extends ExampleModule with ScalaNativeModule { + def scalaNativeVersion = scalaNativeVer + } +} + +// --------------------------------------------------------------------------- +// testLocal: local dev helper, not published +// --------------------------------------------------------------------------- + +object testLocal extends ScalaModule { + override def sources = Task.Sources(os.pwd / "test-local" / "src" / "main" / "scala") + def scalaVersion = scalaVer + override def moduleDeps = Seq(exampleJsoniterJson.jvm) + override def mvnDeps = Task { + Seq( + mvn"com.softwaremill.sttp.client4::core:$sttpClient4Ver", + mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:$jsoniterVer", + mvn"dev.zio::zio-json:$zioJsonVer", + mvn"dev.zio::zio:$zioVer" + ) + } + override def compileMvnDeps = Task { + Seq(mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:$jsoniterVer") + } +} + +// --------------------------------------------------------------------------- +// Test modules: one per (api, version, httpSource, jsonCodec, arrayType) combo +// --------------------------------------------------------------------------- + +type TestKey = (String, String, String, String, String) + +val testCombos: Seq[TestKey] = for { + (apiName, apiVersion) <- Seq( + "pubsub" -> "v1", + "storage" -> "v1", + "aiplatform" -> "v1", + "iamcredentials" -> "v1", + "redis" -> "v1", + "sheets" -> "v4" + ) + httpSource <- Seq("Sttp4") + jsonCodec <- Seq("ZioJson", "Jsoniter") + arrayType <- Seq("ZioChunk", "List") +} yield (apiName, apiVersion, httpSource, jsonCodec, arrayType) + +object tests extends Cross[TestModule](testCombos) +trait TestModule extends Cross.Module5[String, String, String, String, String] with ScalaModule { + val apiName = crossValue + val apiVersion = crossValue2 + val httpSource = crossValue3 + val jsonCodec = crossValue4 + val arrayType = crossValue5 + + def scalaVersion = scalaVer + + override def scalacOptions: T[Seq[String]] = + Task { super.scalacOptions() ++ Seq("-Xmax-inlines:64") } + + override def sources: T[Seq[PathRef]] = Task { Seq.empty[PathRef] } + + override def moduleDeps: Seq[JavaModule] = + if (jsonCodec == "Jsoniter") Seq(exampleJsoniterJson.jvm) else Seq() + + override def mvnDeps: T[Seq[Dep]] = Task { + Seq(mvn"com.softwaremill.sttp.client4::core:$sttpClient4Ver") ++ + (jsonCodec match { + case "Jsoniter" => Seq(mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:$jsoniterVer") + case "ZioJson" => Seq(mvn"dev.zio::zio-json:$zioJsonVer") + case other => sys.error(s"Unknown jsonCodec: $other") + }) ++ + (arrayType match { + case "ZioChunk" => Seq(mvn"dev.zio::zio:$zioVer") + case _ => Seq.empty + }) + } + + override def compileMvnDeps: T[Seq[Dep]] = Task { + if (jsonCodec == "Jsoniter") + Seq(mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:$jsoniterVer") + else Seq.empty + } + + override def generatedSources: T[Seq[PathRef]] = Task { + val dest = Task.dest + val basePkg = s"gcp.$apiName.$apiVersion.${httpSource}_${jsonCodec}_${arrayType}".toLowerCase() + val outSrcDir = dest / "scala" + val specFile = + os.pwd / "modules" / "test-resources" / "src" / "main" / "resources" / s"${apiName}_${apiVersion}.json" + os.makeDir.all(outSrcDir) + os.proc( + cliBinPath, + s"-specs=$specFile", + s"-out-dir=$outSrcDir", + s"-out-pkg=$basePkg", + s"-http-source=$httpSource", + s"-json-codec=$jsonCodec", + s"-array-type=$arrayType", + s"-jsoniter-json-type=_root_.example.jsoniter.Json" + ).call(cwd = os.pwd) + Seq(PathRef(dest)) + } + + object test extends ScalaTests { + override def mvnDeps: T[Seq[Dep]] = Task { + Seq( + mvn"org.scalameta::munit:$munitVer", + mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:$jsoniterVer" + ) + } + def testFramework = "munit.Framework" + } +} diff --git a/mill b/mill new file mode 100755 index 0000000..5268dce --- /dev/null +++ b/mill @@ -0,0 +1,199 @@ +#!/usr/bin/env sh + +set -e + +if [ -z "${DEFAULT_MILL_VERSION}" ] ; then DEFAULT_MILL_VERSION="1.1.6"; fi + +if [ -z "${GITHUB_RELEASE_CDN}" ] ; then GITHUB_RELEASE_CDN=""; fi + +if [ -z "$MILL_MAIN_CLI" ] ; then MILL_MAIN_CLI="${0}"; fi + +MILL_REPO_URL="https://github.com/com-lihaoyi/mill" + +MILL_BUILD_SCRIPT="" + +if [ -f "build.mill" ] ; then + MILL_BUILD_SCRIPT="build.mill" +elif [ -f "build.mill.scala" ] ; then + MILL_BUILD_SCRIPT="build.mill.scala" +elif [ -f "build.sc" ] ; then + MILL_BUILD_SCRIPT="build.sc" +fi + +# `s/.*://`: +# This is a greedy match that removes everything from the beginning of the line up to (and including) the last +# colon (:). This effectively isolates the value part of the declaration. +# +# `s/#.*//`: +# This removes any comments at the end of the line. +# +# `s/['\"]//g`: +# This removes all single and double quotes from the string, wherever they appear (g is for "global"). +# +# `s/^[[:space:]]*//; s/[[:space:]]*$//`: +# These two expressions trim any leading or trailing whitespace ([[:space:]] matches spaces and tabs). +TRIM_VALUE_SED="s/.*://; s/#.*//; s/['\"]//g; s/^[[:space:]]*//; s/[[:space:]]*$//" + +if [ -z "${MILL_VERSION}" ] ; then + if [ -f ".mill-version" ] ; then + MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" + elif [ -f ".config/mill-version" ] ; then + MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" + elif [ -f "build.mill.yaml" ] ; then + MILL_VERSION="$(grep -E "mill-version:" "build.mill.yaml" | sed -E "$TRIM_VALUE_SED")" + elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then + MILL_VERSION="$(grep -E "//\|.*mill-version" "${MILL_BUILD_SCRIPT}" | sed -E "$TRIM_VALUE_SED")" + fi +fi + +if [ -z "${MILL_VERSION}" ] ; then MILL_VERSION="${DEFAULT_MILL_VERSION}"; fi + +MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" + +if [ -z "${MILL_FINAL_DOWNLOAD_FOLDER}" ] ; then MILL_FINAL_DOWNLOAD_FOLDER="${MILL_USER_CACHE_DIR}/download"; fi + +MILL_NATIVE_SUFFIX="-native" +MILL_JVM_SUFFIX="-jvm" +ARTIFACT_SUFFIX="" + +# Check if GLIBC version is at least the required version +# Returns 0 (true) if GLIBC >= required version, 1 (false) otherwise +check_glibc_version() { + required_version="2.39" + required_major=$(echo "$required_version" | cut -d. -f1) + required_minor=$(echo "$required_version" | cut -d. -f2) + # Get GLIBC version from ldd --version (first line contains version like "ldd (GNU libc) 2.31") + glibc_version=$(ldd --version 2>/dev/null | head -n 1 | grep -oE '[0-9]+\.[0-9]+$' || echo "") + if [ -z "$glibc_version" ]; then + # If we can't determine GLIBC version, assume it's too old + return 1 + fi + glibc_major=$(echo "$glibc_version" | cut -d. -f1) + glibc_minor=$(echo "$glibc_version" | cut -d. -f2) + if [ "$glibc_major" -gt "$required_major" ]; then + return 0 + elif [ "$glibc_major" -eq "$required_major" ] && [ "$glibc_minor" -ge "$required_minor" ]; then + return 0 + else + return 1 + fi +} + +set_artifact_suffix() { + if [ "$(uname -s 2>/dev/null | cut -c 1-5)" = "Linux" ]; then + # Native binaries require new enough GLIBC; fall back to JVM launcher if older + if ! check_glibc_version; then + return + fi + if [ "$(uname -m)" = "aarch64" ]; then ARTIFACT_SUFFIX="-native-linux-aarch64" + else ARTIFACT_SUFFIX="-native-linux-amd64"; fi + elif [ "$(uname)" = "Darwin" ]; then + if [ "$(uname -m)" = "arm64" ]; then ARTIFACT_SUFFIX="-native-mac-aarch64" + else ARTIFACT_SUFFIX="-native-mac-amd64"; fi + else + echo "This native mill launcher supports only Linux and macOS." 1>&2 + exit 1 + fi +} + +case "$MILL_VERSION" in + *"$MILL_NATIVE_SUFFIX") + MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} + set_artifact_suffix + ;; + + *"$MILL_JVM_SUFFIX") + MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} + ;; + + *) + case "$MILL_VERSION" in + 0.1.* | 0.2.* | 0.3.* | 0.4.* | 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.* | 0.12.*) + ;; + *) + set_artifact_suffix + ;; + esac + ;; +esac + +MILL="${MILL_FINAL_DOWNLOAD_FOLDER}/$MILL_VERSION$ARTIFACT_SUFFIX" + +# If not already downloaded, download it +if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then + case $MILL_VERSION in + 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.*) + MILL_DOWNLOAD_SUFFIX="" + MILL_DOWNLOAD_FROM_MAVEN=0 + ;; + 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M*) + MILL_DOWNLOAD_SUFFIX="-assembly" + MILL_DOWNLOAD_FROM_MAVEN=0 + ;; + *) + MILL_DOWNLOAD_SUFFIX="-assembly" + MILL_DOWNLOAD_FROM_MAVEN=1 + ;; + esac + case $MILL_VERSION in + 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11) + MILL_DOWNLOAD_EXT="jar" + ;; + 0.12.*) + MILL_DOWNLOAD_EXT="exe" + ;; + 0.*) + MILL_DOWNLOAD_EXT="jar" + ;; + *) + MILL_DOWNLOAD_EXT="exe" + ;; + esac + + MILL_TEMP_DOWNLOAD_FILE="${MILL_OUTPUT_DIR:-out}/mill-temp-download" + mkdir -p "$(dirname "${MILL_TEMP_DOWNLOAD_FILE}")" + + if [ "$MILL_DOWNLOAD_FROM_MAVEN" = "1" ] ; then + MILL_DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${MILL_DOWNLOAD_EXT}" + else + MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') + MILL_DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${MILL_DOWNLOAD_SUFFIX}" + unset MILL_VERSION_TAG + fi + + + if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then + echo "$MILL_DOWNLOAD_URL" + echo "$MILL" + exit 0 + fi + + echo "Downloading mill ${MILL_VERSION} from ${MILL_DOWNLOAD_URL} ..." 1>&2 + curl -f -L -o "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL_DOWNLOAD_URL}" + + chmod +x "${MILL_TEMP_DOWNLOAD_FILE}" + + mkdir -p "${MILL_FINAL_DOWNLOAD_FOLDER}" + mv "${MILL_TEMP_DOWNLOAD_FILE}" "${MILL}" + + unset MILL_TEMP_DOWNLOAD_FILE + unset MILL_DOWNLOAD_SUFFIX +fi + +MILL_FIRST_ARG="" +if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--help" ] ; then + # Need to preserve the first position of those listed options + MILL_FIRST_ARG=$1 + shift +fi + +unset MILL_FINAL_DOWNLOAD_FOLDER +unset MILL_OLD_DOWNLOAD_PATH +unset OLD_MILL +unset MILL_VERSION +unset MILL_REPO_URL + +# -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 +# We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes +# shellcheck disable=SC2086 +exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" diff --git a/modules/cli/src/main/scala/cli.scala b/modules/cli/src/main/scala/main.scala similarity index 97% rename from modules/cli/src/main/scala/cli.scala rename to modules/cli/src/main/scala/main.scala index dd20351..c81be95 100644 --- a/modules/cli/src/main/scala/cli.scala +++ b/modules/cli/src/main/scala/main.scala @@ -1,7 +1,7 @@ // for test runs using scala-cli //> using jvm system -//> using scala 3.8.2 -//> using file ../../../../core/shared/src/main/scala/codegen.scala +//> using scala 3.8.3 +//> using file ../../../../core/src/main/scala/codegen.scala //> using dep com.lihaoyi::upickle:4.4.3 package gcp.codegen.cli diff --git a/modules/core/shared/src/main/scala/codegen.scala b/modules/core/src/main/scala/codegen.scala similarity index 100% rename from modules/core/shared/src/main/scala/codegen.scala rename to modules/core/src/main/scala/codegen.scala diff --git a/modules/example-jsoniter-json/shared/src/main/scala/json.scala b/modules/example-jsoniter-json/src/main/scala/json.scala similarity index 100% rename from modules/example-jsoniter-json/shared/src/main/scala/json.scala rename to modules/example-jsoniter-json/src/main/scala/json.scala diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index 01a16ed..0000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.11.7 diff --git a/build.sbt b/sbt_backup/build_sbt.bak similarity index 85% rename from build.sbt rename to sbt_backup/build_sbt.bak index 5f3dc24..f9f362a 100644 --- a/build.sbt +++ b/sbt_backup/build_sbt.bak @@ -2,7 +2,7 @@ ThisBuild / description := "Google Cloud client code generator" ThisBuild / organization := "dev.rolang" ThisBuild / licenses := Seq(License.MIT) ThisBuild / homepage := Some(url("https://github.com/rolang/google-rest-api-codegen")) -ThisBuild / scalaVersion := "3.8.2" +ThisBuild / scalaVersion := "3.8.3" ThisBuild / version ~= { v => if (v.contains('+')) s"${v.replace('+', '-')}-SNAPSHOT" else v } ThisBuild / versionScheme := Some("early-semver") ThisBuild / scmInfo := Some( @@ -40,34 +40,30 @@ lazy val noPublish = Seq( publish / skip := true ) -val sttpClient4Version = "4.0.13" +val sttpClient4Version = "4.0.23" -val zioVersion = "2.1.23" +val zioVersion = "2.1.26" -val zioJsonVersion = "0.8.0" +val zioJsonVersion = "0.9.1" -val jsoniterVersion = "2.38.8" +val jsoniterVersion = "2.38.11" -val munitVersion = "1.2.1" +val munitVersion = "1.3.0" val upickleVersion = "4.4.3" -addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") +addCommandAlias("fmt", "all scalafmtSbt scalafmt Test/scalafmt") lazy val root = (project in file(".")) .aggregate( - core.native, - core.jvm, - exampleJsoniterJson.native, - exampleJsoniterJson.jvm, - cli + (core.projectRefs ++ exampleJsoniterJson.projectRefs ++ Seq(LocalProject("cli")) ++ testProjects.componentProjects + .map(p => LocalProject(p.id))) * ) - .aggregate(testProjects.componentProjects.map(p => LocalProject(p.id)) *) .settings(noPublish) // for supporting code inspection / testing of generated code via test_gen.sh script lazy val testLocal = (project in file("test-local")) - .dependsOn(exampleJsoniterJson.jvm) + .dependsOn(LocalProject("exampleJsoniterJson")) .settings( libraryDependencies ++= Seq( "com.softwaremill.sttp.client4" %% "core" % sttpClient4Version, @@ -79,8 +75,7 @@ lazy val testLocal = (project in file("test-local")) ) .settings(noPublish) -lazy val core = crossProject(JVMPlatform, NativePlatform) - .in(file("modules/core")) +lazy val core = (projectMatrix in file("modules/core")) .settings(publishSettings) .settings( name := "gcp-codegen", @@ -88,24 +83,25 @@ lazy val core = crossProject(JVMPlatform, NativePlatform) ) .settings( libraryDependencies ++= Seq( - "com.lihaoyi" %%% "upickle" % upickleVersion + "com.lihaoyi" %% "upickle" % upickleVersion ) ) + .jvmPlatform(scalaVersions = Seq("3.8.3")) + .nativePlatform(scalaVersions = Seq("3.8.3")) lazy val cli = project .in(file("modules/cli")) - .aggregate(core.native) - .dependsOn(core.native) + .aggregate(LocalProject("coreNative")) + .dependsOn(LocalProject("coreNative")) .enablePlugins(ScalaNativePlugin) .settings(publishSettings) .settings( name := "gcp-codegen-cli", moduleName := "gcp-codegen-cli", - nativeConfig := nativeConfig.value.withMultithreading(false) + nativeConfig ~= { _.withMultithreading(false) } ) -lazy val exampleJsoniterJson = crossProject(JVMPlatform, NativePlatform) - .in(file("modules/example-jsoniter-json")) +lazy val exampleJsoniterJson = (projectMatrix in file("modules/example-jsoniter-json")) .settings(noPublish) .settings( name := "example-jsoniter-json", @@ -117,6 +113,8 @@ lazy val exampleJsoniterJson = crossProject(JVMPlatform, NativePlatform) "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % jsoniterVersion % "compile-internal" ) ) + .jvmPlatform(scalaVersions = Seq("3.8.3")) + .nativePlatform(scalaVersions = Seq("3.8.3")) def dependencyByConfig(httpSource: String, jsonCodec: String, arrayType: String): Seq[ModuleID] = { (httpSource match { @@ -182,7 +180,7 @@ lazy val testProjects: CompositeProject = new CompositeProject { } lazy val cliBinFile: File = { - val cliTarget = new File("modules/cli/target/bin") + val cliTarget = new File("target/bin") def normalise(s: String) = s.toLowerCase.replaceAll("[^a-z0-9]+", "") val props = sys.props.toMap @@ -208,8 +206,8 @@ lazy val cliBinFile: File = { } lazy val buildCliBinary = taskKey[File]("") -buildCliBinary := { - val built = (cli / Compile / nativeLinkReleaseFast).value +buildCliBinary := Def.uncached { + val built = java.io.File((cli / Compile / nativeLinkReleaseFast).value.toString.replace(s"$${OUT}", "target/out")) IO.copyFile(built, cliBinFile) cliBinFile } @@ -231,11 +229,11 @@ def codegenTask( val outSrcDir = outDir / "scala" @scala.annotation.tailrec - def listFilesRec(dir: List[File], res: List[File]): List[File] = + def listFilesRec(dir: List[File], res: Seq[File]): Seq[File] = dir match { case x :: xs => val (dirs, files) = IO.listFiles(x).toList.partition(_.isDirectory()) - listFilesRec(dirs ::: xs, files ::: res) + listFilesRec(dirs ::: xs, files ++ res) case Nil => res } @@ -267,6 +265,6 @@ def codegenTask( val files = listFilesRec(List(outDir), Nil) IO.delete(outDir / ".scala-build") - files + files.toSeq } } diff --git a/sbt_backup/project/build_properties.bak b/sbt_backup/project/build_properties.bak new file mode 100644 index 0000000..ab6f7c0 --- /dev/null +++ b/sbt_backup/project/build_properties.bak @@ -0,0 +1 @@ +sbt.version=2.0.0-RC12 diff --git a/project/plugins.sbt b/sbt_backup/project/plugins_sbt.bak similarity index 54% rename from project/plugins.sbt rename to sbt_backup/project/plugins_sbt.bak index 42d75da..676c711 100644 --- a/project/plugins.sbt +++ b/sbt_backup/project/plugins_sbt.bak @@ -1,9 +1,7 @@ -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.9") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.11") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") -addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") - addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") From 5798bd9ee780c39211dfef69c0a80c45fd08a1e6 Mon Sep 17 00:00:00 2001 From: Roman Langolf Date: Sun, 31 May 2026 15:02:36 +0700 Subject: [PATCH 2/6] bump versions --- build.mill | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.mill b/build.mill index eb99027..8121675 100644 --- a/build.mill +++ b/build.mill @@ -11,7 +11,7 @@ import os.Path val scalaVer = "3.8.3" -val scalaNativeVer = "0.5.11" +val scalaNativeVer = "0.5.12" val sttpClient4Ver = "4.0.23" @@ -19,7 +19,7 @@ val zioVer = "2.1.26" val zioJsonVer = "0.9.1" -val jsoniterVer = "2.38.11" +val jsoniterVer = "2.38.12" val munitVer = "1.3.0" From 212f10c30b8ac6358141f619485fa384d3ad2829 Mon Sep 17 00:00:00 2001 From: Roman Langolf Date: Sun, 31 May 2026 15:31:15 +0700 Subject: [PATCH 3/6] fix stack overflow in build --- build.mill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.mill b/build.mill index 8121675..2e7869e 100644 --- a/build.mill +++ b/build.mill @@ -111,7 +111,7 @@ object cli extends ScalaNativeModule with Publishable with SbtModule { object exampleJsoniterJson extends Module { trait ExampleModule extends ScalaModule with SbtModule { - def moduleDir = moduleDir / ".." / ".." / "modules" / "example-jsoniter-json" + def moduleDir = os.pwd / "modules" / "example-jsoniter-json" def scalaVersion = scalaVer override def mvnDeps = Task { Seq(mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:$jsoniterVer") } From 5b29bb1de30e5b6e48b9061054b330123f7172c6 Mon Sep 17 00:00:00 2001 From: Roman Langolf Date: Sun, 31 May 2026 17:53:54 +0700 Subject: [PATCH 4/6] fix: use override def moduleDir in ExampleModule to prevent empty allSourceFiles The ExampleModule trait defined moduleDir as a non-overriding def with an absolute path, causing millSourcePath to dispatch to the parent's default moduleDir. This left sources.dest empty and allSourceFiles empty, so no .class files were produced and example.jsoniter.Json was missing from the classpath (185 compilation errors). Fix by using override def with the same relative-path pattern as CoreModule. Also fix specFile path and cliBin usage. --- build.mill | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/build.mill b/build.mill index 2e7869e..c34e36a 100644 --- a/build.mill +++ b/build.mill @@ -111,8 +111,7 @@ object cli extends ScalaNativeModule with Publishable with SbtModule { object exampleJsoniterJson extends Module { trait ExampleModule extends ScalaModule with SbtModule { - def moduleDir = os.pwd / "modules" / "example-jsoniter-json" - + override def moduleDir: Path = super.moduleDir / ".." / ".." / "modules" / "example-jsoniter-json" def scalaVersion = scalaVer override def mvnDeps = Task { Seq(mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:$jsoniterVer") } override def compileMvnDeps = Task { @@ -189,9 +188,10 @@ trait TestModule extends Cross.Module5[String, String, String, String, String] w override def mvnDeps: T[Seq[Dep]] = Task { Seq(mvn"com.softwaremill.sttp.client4::core:$sttpClient4Ver") ++ (jsonCodec match { - case "Jsoniter" => Seq(mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:$jsoniterVer") - case "ZioJson" => Seq(mvn"dev.zio::zio-json:$zioJsonVer") - case other => sys.error(s"Unknown jsonCodec: $other") + case "Jsoniter" => + Seq(mvn"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:$jsoniterVer") + case "ZioJson" => Seq(mvn"dev.zio::zio-json:$zioJsonVer") + case other => sys.error(s"Unknown jsonCodec: $other") }) ++ (arrayType match { case "ZioChunk" => Seq(mvn"dev.zio::zio:$zioVer") @@ -210,10 +210,14 @@ trait TestModule extends Cross.Module5[String, String, String, String, String] w val basePkg = s"gcp.$apiName.$apiVersion.${httpSource}_${jsonCodec}_${arrayType}".toLowerCase() val outSrcDir = dest / "scala" val specFile = - os.pwd / "modules" / "test-resources" / "src" / "main" / "resources" / s"${apiName}_${apiVersion}.json" + // os.pwd / "modules" / "test-resources" / "src" / "main" / "resources" / s"${apiName}_${apiVersion}.json" + s"/home/roman/projects/rolang/google-api-codegen/modules/test-resources/src/main/resources/${apiName}_${apiVersion}.json" + + val cliBin = buildCliBinary() + os.makeDir.all(outSrcDir) os.proc( - cliBinPath, + cliBin.path, s"-specs=$specFile", s"-out-dir=$outSrcDir", s"-out-pkg=$basePkg", From 1acb73d77319e4b9e2f9debbf2e425189f0a412b Mon Sep 17 00:00:00 2001 From: Roman Langolf Date: Sun, 31 May 2026 17:56:56 +0700 Subject: [PATCH 5/6] ci: switch from sbt to mill in GitHub Actions - Replace VirtusLab/scala-cli-setup with actions/setup-java (no sbt needed) - Replace 'sbt buildCliBinary test' with './mill tests.__.test' - Replace 'sbt publishSigned sonaRelease' with './mill core.{jvm,native}.publish cli.publish' The mill wrapper script in the project root bootstraps Mill 1.1.6 automatically. --- .github/workflows/ci.yml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9e77f9..1a53cd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,14 +13,13 @@ jobs: steps: - uses: actions/checkout@v5 - uses: coursier/cache-action@v6 - - uses: VirtusLab/scala-cli-setup@main + - uses: actions/setup-java@v4 with: - jvm: temurin:25 - apps: sbt - power: true + java-version: 25 + distribution: temurin - name: Start up emulators - run: docker compose up -d - - run: sbt buildCliBinary test + run: docker compose up -d + - run: ./mill tests.__.test publish: runs-on: ubuntu-latest name: Publish Artifacts @@ -29,17 +28,16 @@ jobs: steps: - uses: actions/checkout@v5 - uses: coursier/cache-action@v6 - - uses: VirtusLab/scala-cli-setup@main + - uses: actions/setup-java@v4 with: - jvm: temurin:25 - apps: sbt + java-version: 25 + distribution: temurin - name: Import signing key if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' env: PGP_SECRET: ${{ secrets.PGP_SECRET }} PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} run: echo $PGP_SECRET | base64 -d -i - | gpg --import - - name: Import signing key and strip passphrase if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' env: @@ -53,4 +51,8 @@ jobs: env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - run: sbt publishSigned sonaRelease + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: | + ./mill core.jvm.publish + ./mill core.native.publish + ./mill cli.publish From 646675ac6f3e12a2253f88c04f32a48b39c13846 Mon Sep 17 00:00:00 2001 From: Roman Langolf Date: Sun, 31 May 2026 18:30:20 +0700 Subject: [PATCH 6/6] fix: replace hardcoded absolute path with TaskCtx.workspace, fix sandbox violations - Replace hardcoded /home/roman/... path with TaskCtx.workspace (Mill's official API for the project root inside tasks) - Remove cliBinPath (wrote outside Task.dest, hit sandbox restriction); buildCliBinary now writes to Task.dest/cli - Fix testLocal sources to use moduleDir instead of os.pwd - Add import mill.api.TaskCtx --- build.mill | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/build.mill b/build.mill index c34e36a..4ab4c3c 100644 --- a/build.mill +++ b/build.mill @@ -7,6 +7,7 @@ import mill.scalanativelib.api.* import mill.scalalib.publish.* import mill.util.VcsVersionModule import mill.api.Task.Simple +import mill.api.TaskCtx import os.Path val scalaVer = "3.8.3" @@ -25,30 +26,10 @@ val munitVer = "1.3.0" val upickleVer = "4.4.3" -def cliBinPath: os.Path = { - def normalise(s: String) = s.toLowerCase.replaceAll("[^a-z0-9]+", "") - val props = sys.props.toMap - val osName = normalise(props.getOrElse("os.name", "")) match { - case p if p.startsWith("linux") => "linux" - case p if p.startsWith("windows") => "windows" - case p if p.startsWith("osx") || p.startsWith("macosx") => "macosx" - case _ => "unknown" - } - val arch = ( - normalise(props.getOrElse("os.arch", "")), - props.getOrElse("sun.arch.data.model", "64") - ) match { - case ("amd64" | "x64" | "x8664" | "x86", bits) => s"x86_$bits" - case ("aarch64" | "arm64", bits) => s"aarch$bits" - case _ => "unknown" - } - os.pwd / "target" / "bin" / s"gcp-codegen-$arch-$osName" -} - -/** Build the Scala Native CLI binary and copy it to target/bin/- */ +/** Build the Scala Native CLI binary */ def buildCliBinary: T[PathRef] = Task { val linked = cli.nativeLink() - val dest = cliBinPath + val dest = Task.dest / "cli" os.makeDir.all(dest / os.up) os.copy.over(linked.path, dest) PathRef(dest) @@ -131,7 +112,7 @@ object exampleJsoniterJson extends Module { // --------------------------------------------------------------------------- object testLocal extends ScalaModule { - override def sources = Task.Sources(os.pwd / "test-local" / "src" / "main" / "scala") + override def sources = Task.Sources(moduleDir / "src" / "main" / "scala") def scalaVersion = scalaVer override def moduleDeps = Seq(exampleJsoniterJson.jvm) override def mvnDeps = Task { @@ -209,9 +190,9 @@ trait TestModule extends Cross.Module5[String, String, String, String, String] w val dest = Task.dest val basePkg = s"gcp.$apiName.$apiVersion.${httpSource}_${jsonCodec}_${arrayType}".toLowerCase() val outSrcDir = dest / "scala" + val ws = implicitly[TaskCtx].workspace val specFile = - // os.pwd / "modules" / "test-resources" / "src" / "main" / "resources" / s"${apiName}_${apiVersion}.json" - s"/home/roman/projects/rolang/google-api-codegen/modules/test-resources/src/main/resources/${apiName}_${apiVersion}.json" + ws / "modules" / "test-resources" / "src" / "main" / "resources" / s"${apiName}_${apiVersion}.json" val cliBin = buildCliBinary() @@ -225,7 +206,7 @@ trait TestModule extends Cross.Module5[String, String, String, String, String] w s"-json-codec=$jsonCodec", s"-array-type=$arrayType", s"-jsoniter-json-type=_root_.example.jsoniter.Json" - ).call(cwd = os.pwd) + ).call(cwd = ws) Seq(PathRef(dest)) }