From 46ec2c5e688d9729981ed281f3be78dc13a111fe Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Sun, 31 May 2026 18:59:20 +0200 Subject: [PATCH 1/2] Migrate from archived NGINX Unit to FreeUnit. Use FreeUnit 1.35.5 as the unitd server in CI and docs, building from source since official packages are not yet available. Co-authored-by: Cursor --- .github/scripts/install-freeunit.sh | 25 ++++ .github/workflows/build.yml | 16 +-- README.md | 10 +- build.mill.scala | 2 +- docs/index.html | 4 +- sbt-plugin/src/main/scala/SNUnitPlugin.scala | 9 +- .../src/sbt-test/snunit-plugin/simple/test | 2 +- .../src/snunit/plugin/SNUnit.scala | 109 +++++++++++------- .../snunit/plugin/SNUnitMillPluginTests.scala | 4 +- snunit/src/snunit/unsafe/unsafe.scala | 2 +- versions.mill.scala | 3 +- 11 files changed, 116 insertions(+), 70 deletions(-) create mode 100755 .github/scripts/install-freeunit.sh diff --git a/.github/scripts/install-freeunit.sh b/.github/scripts/install-freeunit.sh new file mode 100755 index 00000000..919ad264 --- /dev/null +++ b/.github/scripts/install-freeunit.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +FREEUNIT_VERSION="${FREEUNIT_VERSION:-1.35.5}" +PREFIX="${FREEUNIT_PREFIX:-/usr/local}" + +sudo apt-get update +sudo apt-get install -y libuv1-dev libidn2-dev libpcre2-dev libssl-dev + +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +curl -sL "https://github.com/freeunitorg/freeunit/archive/refs/tags/${FREEUNIT_VERSION}.tar.gz" \ + | tar xz -C "$tmpdir" --strip-components=1 + +( + cd "$tmpdir" + ./configure --openssl --otel + make build/sbin/unitd build/lib/libunit.a + sudo install -d "$PREFIX/bin" "$PREFIX/lib" + sudo install -m 755 build/sbin/unitd "$PREFIX/bin/unitd" + sudo install -m 644 build/lib/libunit.a "$PREFIX/lib/libunit.a" +) + +unitd --version diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e546b6e7..aa45aaf8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,14 +13,8 @@ jobs: with: path: ./out key: out-folder-cache - - name: Install Dependencies - run: | - curl -sL https://nginx.org/keys/nginx_signing.key | sudo apt-key add - - echo "deb https://packages.nginx.org/unit/ubuntu/ noble unit" | sudo tee -a /etc/apt/sources.list.d/unit.list - echo "deb-src https://packages.nginx.org/unit/ubuntu/ noble unit" | sudo tee -a /etc/apt/sources.list.d/unit.list - sudo apt-get update - unit_version="1.34.*" - sudo apt-get install -y libuv1-dev libidn2-dev unit=$unit_version unit-dev=$unit_version + - name: Install FreeUnit + run: bash .github/scripts/install-freeunit.sh - name: Download dependencies run: | ./mill __.prepareOffline @@ -37,11 +31,7 @@ jobs: - name: Run Unit Tests run: ./mill snunit.test - name: Run Integration Tests - run: | - sudo systemctl stop unit.service - ./mill integration.test - - name: Start NGINX Unit daemon - run: sudo systemctl start unit.service + run: ./mill integration.test # TODO: Enable again # - name: Run Mill Plugin Tests # run: ./mill -i snunit-mill-plugin.__.test diff --git a/README.md b/README.md index ff00542b..e8b1887b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SNUnit: Scala Native HTTP server based on NGINX Unit +# SNUnit: Scala Native HTTP server based on FreeUnit ```scala import snunit.* @@ -18,10 +18,12 @@ def run = ``` SNUnit is a Scala Native library to write HTTP server applications on top of -[NGINX Unit](https://unit.nginx.org/). It allows you to write both synchronous +[FreeUnit](https://freeunit.org/) (community LTS fork of NGINX Unit). It allows you to write both synchronous and asynchronous web servers with automatic restart on crashes, automatic load balancing of multiple processes, great performance and all the nice -[NGINX Unit features](http://unit.nginx.org/#key-features). +[FreeUnit features](https://freeunit.org/). + +FreeUnit is a drop-in replacement for the archived [NGINX Unit](https://unit.nginx.org/) — same `unitd` binary name, control API, and config schema. Install FreeUnit's `unitd` package instead of the archived NGINX Unit one, or use the Docker image `ghcr.io/freeunitorg/freeunit:latest-minimal`. ## Running your app @@ -58,7 +60,7 @@ to the `unitd` working directory. This configuration passes all requests sent to the port `8081` to the application `myapp`. -To know more about configuring NGINX Unit, refer to [its documentation](http://unit.nginx.org/configuration). +To know more about configuring FreeUnit, refer to [its documentation](https://github.com/freeunitorg/freeunit#documentation). To deploy the setting you can use curl: diff --git a/build.mill.scala b/build.mill.scala index 42df6ab2..8a66450f 100644 --- a/build.mill.scala +++ b/build.mill.scala @@ -73,7 +73,7 @@ object Common { trait Publish extends CiReleaseModule with Mima { def pomSettings = PomSettings( - description = "Scala Native server using NGINX Unit", + description = "Scala Native server using FreeUnit", organization = "com.github.lolgab", url = "https://github.com/lolgab/snunit", licenses = Seq(License.`Apache-2.0`), diff --git a/docs/index.html b/docs/index.html index 3bd0cf5d..765aa64b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,9 +2,9 @@ - SNUnit | Scala Native NGINX Unit + SNUnit | Scala Native FreeUnit - + diff --git a/sbt-plugin/src/main/scala/SNUnitPlugin.scala b/sbt-plugin/src/main/scala/SNUnitPlugin.scala index 274cb886..443d9e89 100644 --- a/sbt-plugin/src/main/scala/SNUnitPlugin.scala +++ b/sbt-plugin/src/main/scala/SNUnitPlugin.scala @@ -16,7 +16,9 @@ object SNUnitPlugin extends AutoPlugin { val snunitCurlCommand = settingKey[Seq[String]]("curl command to use") val snunitVersion: String = snunit.plugin.internal.BuildInfo.snunitVersion - val deployToNGINXUnit = taskKey[Unit]("Deploy app to NGINX Unit") + val deployToFreeUnit = taskKey[Unit]("Deploy app to FreeUnit") + @deprecated("Use deployToFreeUnit", "SNUnit next release") + val deployToNGINXUnit = deployToFreeUnit } import autoImport._ @@ -34,10 +36,11 @@ object SNUnitPlugin extends AutoPlugin { }, snunitCurlCommand := Seq("curl"), snunitPort := 8080, - deployToNGINXUnit := { + deployToFreeUnit := { val port = snunitPort.value val executable = (Compile / nativeLink).value shared.value.deployToNGINXUnit(executable.toString, port) - } + }, + deployToNGINXUnit := deployToFreeUnit.value ) } diff --git a/sbt-plugin/src/sbt-test/snunit-plugin/simple/test b/sbt-plugin/src/sbt-test/snunit-plugin/simple/test index 50c4e273..250034c7 100644 --- a/sbt-plugin/src/sbt-test/snunit-plugin/simple/test +++ b/sbt-plugin/src/sbt-test/snunit-plugin/simple/test @@ -1,2 +1,2 @@ -> deployToNGINXUnit +> deployToFreeUnit > doCallToServer diff --git a/snunit-mill-plugin/src/snunit/plugin/SNUnit.scala b/snunit-mill-plugin/src/snunit/plugin/SNUnit.scala index 5295df86..0160a730 100644 --- a/snunit-mill-plugin/src/snunit/plugin/SNUnit.scala +++ b/snunit-mill-plugin/src/snunit/plugin/SNUnit.scala @@ -6,19 +6,19 @@ import upickle.default._ trait SNUnit extends ScalaNativeModule { def snunitVersion: String = snunit.plugin.internal.BuildInfo.snunitVersion - def snunitNGINXUnitVersion: Target[String] = Task { "1.34.1" } - def snunitNGINXUnitUser: Target[Option[String]] = Task.Input { T.env.get("USER") } - def snunitNGINXUnitGroup: Target[Option[String]] = Task.Input { T.env.get("GROUP") } - def snunitNGINXUnitSources = Task { - val dir = s"unit-${snunitNGINXUnitVersion()}" - val file = s"$dir.tar.gz" - os.write(Task.dest / file, requests.get.stream(s"https://sources.nginx.org/unit/$file")) - val unitDir = Task.dest / dir - os.proc("tar", "xzf", Task.dest / file).call(cwd = Task.dest) - PathRef(Task.dest / dir) + def snunitFreeUnitVersion: Target[String] = Task { "1.35.5" } + def snunitFreeUnitUser: Target[Option[String]] = Task.Input { T.env.get("USER") } + def snunitFreeUnitGroup: Target[Option[String]] = Task.Input { T.env.get("GROUP") } + def snunitFreeUnitSources = Task { + val version = snunitFreeUnitVersion() + val file = s"freeunit-$version.tar.gz" + val url = s"https://github.com/freeunitorg/freeunit/archive/refs/tags/$version.tar.gz" + os.write(Task.dest / file, requests.get.stream(url)) + os.proc("tar", "xzf", Task.dest / file, "--strip-components=1").call(cwd = Task.dest) + PathRef(Task.dest) } - def snunitNGINXUnitBinary = Task { + def snunitFreeUnitBinary = Task { val platform = System.getProperty("os.name", "unknown").toLowerCase() val openSslParams = @@ -26,21 +26,21 @@ trait SNUnit extends ScalaNativeModule { List("--cc-opt=-I/opt/homebrew/opt/openssl@3/include", "--ld-opt=-L/opt/homebrew/opt/openssl@3/lib") } else Nil - val unitDir = snunitNGINXUnitSources().path + val unitDir = snunitFreeUnitSources().path os.proc( "./configure", "--logdir=./logdir", "--log=/dev/stdout", - snunitNGINXUnitUser().map(user => s"--user=$user"), - snunitNGINXUnitGroup().map(group => s"--group=$group"), + snunitFreeUnitUser().map(user => s"--user=$user"), + snunitFreeUnitGroup().map(group => s"--group=$group"), "--runstatedir=./runstatedir", "--pid=unit.pid", "--control=unix:control.sock", "--modulesdir=./modulesdir", "--statedir=./statedir", "--tmpdir=/tmp", - // "--otel", TODO: Support otel + "--otel", "--openssl", openSslParams ).call(cwd = unitDir, stdout = os.Inherit) @@ -49,14 +49,14 @@ trait SNUnit extends ScalaNativeModule { val libunit = Task.dest / "libunit.a" os.copy(unitDir / "build/sbin/unitd", unitd) os.copy(unitDir / "build/lib/libunit.a", libunit) - SNUnit.NGINXUnitInstallation(unitd = PathRef(unitd), libunit = PathRef(libunit)) + SNUnit.FreeUnitInstallation(unitd = PathRef(unitd), libunit = PathRef(libunit)) } /** Port where SNUnit app runs */ def snunitPort: Target[Int] = Task { 8080 } - def snunitNGINXUnitConfig: Target[String] = + def snunitFreeUnitConfig: Target[String] = s"""{ | "listeners": { | "*:${snunitPort()}": { @@ -71,26 +71,26 @@ trait SNUnit extends ScalaNativeModule { | } |}""".stripMargin - def snunitNGINXUnitWorkdir = Task { + def snunitFreeUnitWorkdir = Task { Task.dest } private def runImpl = Task.Anon { - val wd = snunitNGINXUnitWorkdir() - snunitKillNGINXUnit().apply() + val wd = snunitFreeUnitWorkdir() + snunitKillFreeUnit().apply() val statedir = wd / "statedir" os.makeDir.all(statedir) - val nginxUnit = snunitNGINXUnitBinary().unitd.path - val nginxUnitConfig = snunitNGINXUnitConfig() - os.write.over(statedir / "conf.json", nginxUnitConfig) - (wd, nginxUnit) + val freeUnit = snunitFreeUnitBinary().unitd.path + val freeUnitConfig = snunitFreeUnitConfig() + os.write.over(statedir / "conf.json", freeUnitConfig) + (wd, freeUnit) } - /** Run app on NGINX Unit + /** Run app on FreeUnit */ override def run(args: Task[Args] = Task.Anon(Args())): Command[Unit] = Task.Command { - val (wd, nginxUnit) = runImpl() - os.proc(nginxUnit, "--no-daemon").call(wd, stdout = os.Inherit) + val (wd, freeUnit) = runImpl() + os.proc(freeUnit, "--no-daemon").call(wd, stdout = os.Inherit) () } @@ -98,7 +98,7 @@ trait SNUnit extends ScalaNativeModule { override def runBackground(args: String*): Command[Unit] = Task.Command { val (procUuidPath, procLockfile, procUuid) = _root_.mill.snunitinternal.RunModule.backgroundSetup(Task.dest) - val (wd, nginxUnit) = runImpl() + val (wd, freeUnit) = runImpl() mill.util.Jvm.runSubprocess( mainClass = "mill.scalalib.backgroundwrapper.MillBackgroundWrapper", @@ -111,7 +111,7 @@ trait SNUnit extends ScalaNativeModule { procUuid, "500", "", - nginxUnit.toString, + freeUnit.toString, "--no-daemon" ) ++ args, workingDir = wd, @@ -124,11 +124,11 @@ trait SNUnit extends ScalaNativeModule { () } - def snunitKillNGINXUnit(): Command[Unit] = Task.Command { - val pidFile = snunitNGINXUnitWorkdir() / "unit.pid" + def snunitKillFreeUnit(): Command[Unit] = Task.Command { + val pidFile = snunitFreeUnitWorkdir() / "unit.pid" if (os.exists(pidFile)) { - os.proc("kill", os.read(snunitNGINXUnitWorkdir() / "unit.pid").trim).call(stdout = os.Inherit) + os.proc("kill", os.read(snunitFreeUnitWorkdir() / "unit.pid").trim).call(stdout = os.Inherit) } () @@ -139,25 +139,47 @@ trait SNUnit extends ScalaNativeModule { // } override def nativeLinkingOptions: Target[Seq[String]] = Task { - val unitBinary = snunitNGINXUnitBinary() + val unitBinary = snunitFreeUnitBinary() super.nativeLinkingOptions() ++ Seq( unitBinary.libunit.path.toString ) } + + @deprecated("Use snunitFreeUnitVersion", "SNUnit next release") + def snunitNGINXUnitVersion: Target[String] = snunitFreeUnitVersion + @deprecated("Use snunitFreeUnitUser", "SNUnit next release") + def snunitNGINXUnitUser: Target[Option[String]] = snunitFreeUnitUser + @deprecated("Use snunitFreeUnitGroup", "SNUnit next release") + def snunitNGINXUnitGroup: Target[Option[String]] = snunitFreeUnitGroup + @deprecated("Use snunitFreeUnitSources", "SNUnit next release") + def snunitNGINXUnitSources = snunitFreeUnitSources + @deprecated("Use snunitFreeUnitBinary", "SNUnit next release") + def snunitNGINXUnitBinary = snunitFreeUnitBinary + @deprecated("Use snunitFreeUnitConfig", "SNUnit next release") + def snunitNGINXUnitConfig: Target[String] = snunitFreeUnitConfig + @deprecated("Use snunitFreeUnitWorkdir", "SNUnit next release") + def snunitNGINXUnitWorkdir = snunitFreeUnitWorkdir + @deprecated("Use snunitKillFreeUnit", "SNUnit next release") + def snunitKillNGINXUnit(): Command[Unit] = snunitKillFreeUnit() } object SNUnit { - case class NGINXUnitInstallation(unitd: mill.api.PathRef, libunit: mill.api.PathRef) - object NGINXUnitInstallation { - implicit val rw: ReadWriter[NGINXUnitInstallation] = macroRW + case class FreeUnitInstallation(unitd: mill.api.PathRef, libunit: mill.api.PathRef) + object FreeUnitInstallation { + implicit val rw: ReadWriter[FreeUnitInstallation] = macroRW } + @deprecated("Use FreeUnitInstallation", "SNUnit next release") + type NGINXUnitInstallation = FreeUnitInstallation + @deprecated("Use FreeUnitInstallation", "SNUnit next release") + val NGINXUnitInstallation = FreeUnitInstallation + // TODO: Support programmatic config - case class NGINXUnitConfig( - listeners: NGINXUnitConfig.Listeners, - applications: NGINXUnitConfig.Applications + case class FreeUnitConfig( + listeners: FreeUnitConfig.Listeners, + applications: FreeUnitConfig.Applications ) - object NGINXUnitConfig { + object FreeUnitConfig { case class Listeners() object Listeners { implicit val rw: ReadWriter[Listeners] = macroRW @@ -166,6 +188,11 @@ object SNUnit { object Applications { implicit val rw: ReadWriter[Applications] = macroRW } - implicit val rw: ReadWriter[NGINXUnitConfig] = macroRW + implicit val rw: ReadWriter[FreeUnitConfig] = macroRW } + + @deprecated("Use FreeUnitConfig", "SNUnit next release") + type NGINXUnitConfig = FreeUnitConfig + @deprecated("Use FreeUnitConfig", "SNUnit next release") + val NGINXUnitConfig = FreeUnitConfig } diff --git a/snunit-mill-plugin/test/src/snunit/plugin/SNUnitMillPluginTests.scala b/snunit-mill-plugin/test/src/snunit/plugin/SNUnitMillPluginTests.scala index db08a388..c5a662fc 100644 --- a/snunit-mill-plugin/test/src/snunit/plugin/SNUnitMillPluginTests.scala +++ b/snunit-mill-plugin/test/src/snunit/plugin/SNUnitMillPluginTests.scala @@ -38,9 +38,9 @@ object SNUnitMillPluginTests extends TestSuite { } } if (ended.get()) { - sys.error("NGINX Unit failed to run") + sys.error("FreeUnit failed to run") } - eval(build.snunitKillNGINXUnit()) + eval(build.snunitKillFreeUnit()) } } } diff --git a/snunit/src/snunit/unsafe/unsafe.scala b/snunit/src/snunit/unsafe/unsafe.scala index 4dd1fa21..2ee9cea9 100644 --- a/snunit/src/snunit/unsafe/unsafe.scala +++ b/snunit/src/snunit/unsafe/unsafe.scala @@ -537,7 +537,7 @@ extension (ptr: nxt_unit_init_t_*) { extension (ptr: nxt_websocket_header_t_*) { // Assuming little endianess - // Check NGINX Unit codebase to support big endian platforms as well + // Check FreeUnit codebase to support big endian platforms as well @targetName("websocket_header_t_opcode") @inline def opcodeInternal: Byte = (ptr._1 & ((1 << 4) - 1)).toByte diff --git a/versions.mill.scala b/versions.mill.scala index 79a8486e..7f86dffa 100644 --- a/versions.mill.scala +++ b/versions.mill.scala @@ -19,5 +19,4 @@ object Versions { val sttp = "3.10.1" val pprint = "0.9.0" val castor = "0.3.0" - val scalaJavaTime = "2.6.0" -} + val freeUnit = "1.35.5" From f10896637d78d44f03d9c66e5ff7b2bb46bde654 Mon Sep 17 00:00:00 2001 From: Lorenzo Gabriele Date: Sun, 31 May 2026 19:01:39 +0200 Subject: [PATCH 2/2] Fix syntax error in versions.mill.scala. Restore the missing closing brace and scalaJavaTime version. Co-authored-by: Cursor --- versions.mill.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/versions.mill.scala b/versions.mill.scala index 7f86dffa..4fff8ebe 100644 --- a/versions.mill.scala +++ b/versions.mill.scala @@ -19,4 +19,6 @@ object Versions { val sttp = "3.10.1" val pprint = "0.9.0" val castor = "0.3.0" + val scalaJavaTime = "2.6.0" val freeUnit = "1.35.5" +}