Skip to content
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
25 changes: 25 additions & 0 deletions .github/scripts/install-freeunit.sh
Original file line number Diff line number Diff line change
@@ -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
16 changes: 3 additions & 13 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SNUnit: Scala Native HTTP server based on NGINX Unit
# SNUnit: Scala Native HTTP server based on FreeUnit

```scala
import snunit.*
Expand All @@ -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

Expand Down Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion build.mill.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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`),
Expand Down
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SNUnit | Scala Native NGINX Unit</title>
<title>SNUnit | Scala Native FreeUnit</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="SNUnit is a Scala Native library to build HTTP applications that run on NGINX Unit using the libraries you already know and love.">
<meta name="description" content="SNUnit is a Scala Native library to build HTTP applications that run on FreeUnit using the libraries you already know and love.">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/dark.css" />
</head>
Expand Down
9 changes: 6 additions & 3 deletions sbt-plugin/src/main/scala/SNUnitPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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
)
}
2 changes: 1 addition & 1 deletion sbt-plugin/src/sbt-test/snunit-plugin/simple/test
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
> deployToNGINXUnit
> deployToFreeUnit
> doCallToServer
109 changes: 68 additions & 41 deletions snunit-mill-plugin/src/snunit/plugin/SNUnit.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,41 @@ 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 =
if (platform.contains("mac")) {
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)
Expand All @@ -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()}": {
Expand All @@ -71,34 +71,34 @@ 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)

()
}

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",
Expand All @@ -111,7 +111,7 @@ trait SNUnit extends ScalaNativeModule {
procUuid,
"500",
"<subprocess>",
nginxUnit.toString,
freeUnit.toString,
"--no-daemon"
) ++ args,
workingDir = wd,
Expand All @@ -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)
}

()
Expand All @@ -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
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion snunit/src/snunit/unsafe/unsafe.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions versions.mill.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ object Versions {
val pprint = "0.9.0"
val castor = "0.3.0"
val scalaJavaTime = "2.6.0"
val freeUnit = "1.35.5"
}
Loading