Skip to content

Commit 5fb3ef6

Browse files
Initial commit
1 parent 620f46f commit 5fb3ef6

File tree

11 files changed

+523
-0
lines changed

11 files changed

+523
-0
lines changed

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use_flake

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: ci
2+
3+
on: push
4+
5+
concurrency:
6+
group: ${{ github.ref }}_ci
7+
cancel-in-progress: true
8+
9+
jobs:
10+
build:
11+
name: build
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: DeterminateSystems/nix-installer-action@main
16+
- uses: DeterminateSystems/magic-nix-cache-action@main
17+
- uses: DeterminateSystems/flake-checker-action@main
18+
- name: Build binaries
19+
run: nix develop --command sbt nativeLink
20+
- uses: actions/upload-artifact@v4
21+
with:
22+
path: |
23+
target/scala-3.6.3/unikernel-scala-out
24+
compression-level: 9
25+
overwrite: true
26+

.scalafix.conf

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
rules = [
2+
DisableSyntax
3+
LeakingImplicitClassVal
4+
NoAutoTupling
5+
NoValInForComprehension
6+
OrganizeImports
7+
RedundantSyntax
8+
]
9+
10+
DisableSyntax {
11+
noAsInstanceOf = false
12+
noDefaultArgs = true
13+
noFinalize = true
14+
noFinalVal = true
15+
noIsInstanceOf = true
16+
noNulls = true
17+
noReturns = true
18+
noThrows = true
19+
noUniversalEquality = false
20+
noValPatterns = true
21+
noVars = true
22+
noWhileLoops = true
23+
noXml = true
24+
}
25+
26+
OrganizeImports {
27+
blankLines = Auto
28+
coalesceToWildcardImportThreshold = null
29+
expandRelative = true
30+
groupedImports = Explode
31+
groupExplicitlyImportedImplicitsSeparately = true
32+
groups = ["*"]
33+
importSelectorsOrder = Ascii
34+
importsOrder = Ascii
35+
removeUnused = true
36+
targetDialect = Scala3
37+
}

.scalafmt.conf

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
align.preset = none
2+
align.stripMargin = true
3+
assumeStandardLibraryStripMargin = true
4+
fileOverride."glob:**.sbt".align.preset = most
5+
fileOverride."glob:**.sbt".maxColumn = 120
6+
fileOverride."glob:**.sbt".runner.dialect = scala212
7+
fileOverride."glob:**/project/**.scala".align.preset = most
8+
fileOverride."glob:**/project/**.scala".maxColumn = 120
9+
fileOverride."glob:**/project/**.scala".runner.dialect = scala212
10+
maxColumn = 80
11+
newlines.afterCurlyLambdaParams = never
12+
newlines.avoidForSimpleOverflow=[tooLong, punct, slc]
13+
newlines.avoidInResultType = true
14+
newlines.selectChains = unfold
15+
newlines.beforeCurlyLambdaParams = never
16+
newlines.source = fold
17+
newlines.topLevelStatementBlankLines = [
18+
{
19+
blanks = {
20+
after = 1
21+
before = 1
22+
beforeEndMarker = 0
23+
}
24+
}
25+
]
26+
project.git = true
27+
rewrite.redundantBraces.ifElseExpressions = true
28+
rewrite.redundantBraces.stringInterpolation = true
29+
rewrite.redundantParens.infixSide = all
30+
rewrite.redundantBraces.maxBreaks = 1000
31+
rewrite.rules = [AvoidInfix, PreferCurlyFors, RedundantBraces, RedundantParens, SortModifiers]
32+
rewrite.scala3.convertToNewSyntax = true
33+
rewrite.scala3.removeOptionalBraces = true
34+
rewrite.trailingCommas.style = multiple
35+
runner.dialect = scala3
36+
version = 3.8.1

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Scala Native in NanoVM Unikernel
2+
Exploring Scala Native + Nix Flakes + Nix Devshell + Direnv + NanoVM Unikernel.
3+
4+
* https://scala-native.org/en/latest
5+
* https://github.com/numtide/devshell
6+
* https://nixos-and-flakes.thiscute.world/nixos-with-flakes/introduction-to-flakes
7+
* https://direnv.net
8+
* https://github.com/typelevel/typelevel-nix
9+
* https://nanovms.com
10+
11+
The Nix sets up the development environment with:
12+
1. `clang`: C/C++ LLVM compiler https://clang.llvm.org
13+
1. `jdk`: GraalVM Community Edition: https://www.graalvm.org
14+
1. `metals`: Scala Metals LSP server: https://scalameta.org/metals
15+
1. `sbt`: Scala Built Tool: https://www.scala-sbt.org/index.html
16+
1. `scala-cli`: Scala command-line tool: https://scala-cli.virtuslab.org
17+
1. `scala-fix`: Scala refactoring and linting tool for Scala: https://scalacenter.github.io/scalafix
18+
1. `ops`: NanoVM Unikernel build tool: https://github.com/nanovms/ops
19+
1. `qemu`: QEMU hypervisor: https://www.qemu.org
20+
21+
The `build.sbt` points to `clang` binaries provided by the `devshell`.
22+
23+
You can use this repo without `nix` if all of the above provided by your own environment (but will need to update the [`build.sbt`](./build.sbt)).
24+
25+
As for `nix` users, `cd` into the repository directory and run `nix develop` to drop into the development environment.\
26+
Or, if you have `direnv` installed, simply `cd` into the repository directory and do `direnv allow`.\
27+
Now, whenever you `cd` into the repository directory the development environment will be activated automatically,
28+
and erased when you `cd` out of the repository directory.
29+
30+
## Usage
31+
32+
Build the binary: `sbt nativeLink`. \
33+
34+
Now, let's use the `ops` command to run it as a QEMU virtual machine packaged as unikernel. \
35+
It binds to the port 80, so we'll need `sudo`: `sudo ops run --port 80 ./target/scala-3.6.3/unikernel-scala-out`.
36+
37+
38+
In another terminal window: `curl localhost`. \
39+
Output:
40+
```
41+
Hello from Scala Native NanoVM Unikernel! Your request: Request(method=GET, uri=/, httpVersion=HTTP/1.1, headers=Headers(Host: localhost, User-Agent: curl/8.11.0, Accept: */*), entity=Entity.Empty)
42+
```
43+
44+
Packaging: `ops build ./target/scala-3.6.3/unikernel-scala-out`. \
45+
46+
Verify the image created: `ops image list`. \
47+
Output:
48+
```
49+
100% |████████████████████████████████████████| [0s:0s]
50+
100% |████████████████████████████████████████| [0s:0s]
51+
Bootable image file:/home/igor/.ops/images/unikernel-scala-out.img
52+
```
53+
54+
The resulting image then can be deployed to any cloud hypervisor which uses QEMU, e.g. [DigitalOcean](https://digitalocean.com).: \
55+
1. "Create Droplet".
56+
2. "Choose Image" -> "Custom Images".
57+
3. "Add Image".
58+
4. Upload your image from `~/.ops/images/unikernel-scala-out.img`.
59+
5. Wait for uploading the image and verification.
60+
6. On your image: "More" -> "Start Droplet".
61+
62+
The app is currently deployed as `http://unikernel.igorramazanov.tech`:
63+
```
64+
curl -v http://unikernel.igorramazanov.tech
65+
66+
* Host unikernel.igorramazanov.tech:80 was resolved.
67+
* IPv6: (none)
68+
* IPv4: 138.68.108.40
69+
* Trying 138.68.108.40:80...
70+
* Connected to unikernel.igorramazanov.tech (138.68.108.40) port 80
71+
* using HTTP/1.x
72+
> GET / HTTP/1.1
73+
> Host: unikernel.igorramazanov.tech
74+
> User-Agent: curl/8.11.0
75+
> Accept: */*
76+
>
77+
* Request completely sent off
78+
< HTTP/1.1 200 OK
79+
< Date: Wed, 12 Feb 2025 14:25:39 GMT
80+
< Connection: keep-alive
81+
< Content-Type: text/plain; charset=UTF-8
82+
< Content-Length: 195
83+
<
84+
* Connection #0 to host unikernel.igorramazanov.tech left intact
85+
Hello from Scala Native NanoVM Unikernel! Your request: Request(method=GET, uri=/, httpVersion=HTTP/1.1, headers=Headers(Host: unikernel.igorramazanov.tech, User-Agent: curl/8.11.0, Accept: */*))
86+
```
87+
88+
89+
## CI
90+
This repo uses `nix` based GitHub actions for caching the development environment dependencies,
91+
instead of the traditional approach with [`coursier/setup-action`](https://github.com/coursier/setup-action) and [`coursier/cache-action`](https://github.com/coursier/cache-action):
92+
1. https://github.com/DeterminateSystems/nix-installer-action
93+
1. https://github.com/DeterminateSystems/flake-checker-action
94+
1. https://github.com/DeterminateSystems/magic-nix-cache-action
95+
96+
## TODOs
97+
- [ ] non-Nix friendly.
98+
- [ ] build and package with Nix Flakes.
99+
- [ ] automatically build and publish the ELF and the unikernel image.
100+
- [ ] continuous deployment to DigitalOcean.

build.sbt

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import scala.scalanative.build._
2+
3+
lazy val versions = new {
4+
val cats = "2.11.0" // The last version published against Scala Native 0.4.x
5+
val catsEffect = "3.6.0-RC1"
6+
val fs2 = "3.12.0-RC1"
7+
val http4s = "1.0.0-M44"
8+
val ip4s = "3.6.0"
9+
val log4cats = "2.7.0"
10+
val scala = "3.6.3"
11+
}
12+
13+
def addCommandsAlias(name: String, commands: List[String]) = addCommandAlias(name, commands.mkString(";"))
14+
15+
addCommandsAlias(
16+
"dependencyCheck",
17+
List(
18+
"reload plugins",
19+
"dependencyUpdates",
20+
"reload return",
21+
"dependencyUpdates",
22+
"undeclaredCompileDependencies",
23+
"unusedCompileDependencies",
24+
),
25+
)
26+
27+
addCommandsAlias(
28+
"validate",
29+
List(
30+
"scalafmtSbtCheck",
31+
"scalafmtCheckAll",
32+
"scalafixAll --check",
33+
"undeclaredCompileDependenciesTest",
34+
"unusedCompileDependenciesTest",
35+
"test",
36+
),
37+
)
38+
39+
addCommandsAlias("massage", List("scalafixAll", "scalafmtSbt", "scalafmtAll", "Test/compile"))
40+
41+
/** @todo Make friendly for non-nix folks. */
42+
lazy val dirs = new {
43+
val bin = file(sys.env("DEVSHELL_DIR")) / "bin"
44+
val includes = file(sys.env("C_INCLUDE_PATH"))
45+
val lib = file(sys.env("LIBRARY_PATH"))
46+
}
47+
48+
lazy val `unikernel-scala` = project
49+
.in(file("."))
50+
.enablePlugins(ScalaNativePlugin, BindgenPlugin)
51+
.settings(
52+
version := "0.0.1",
53+
organization := "tech.igorramazanov.unikernel.scala",
54+
scalacOptions :=
55+
List("-deprecation", "-feature", "-new-syntax", "-rewrite", "-unchecked", "-Wall", "-Wunused:imports"),
56+
scalafixOnCompile := true,
57+
semanticdbEnabled := true,
58+
semanticdbVersion := scalafixSemanticdb.revision,
59+
scalaVersion := versions.scala,
60+
Compile / fork := true,
61+
logLevel := Level.Info,
62+
nativeConfig ~=
63+
(config =>
64+
config
65+
.withBuildTarget(BuildTarget.application)
66+
.withCheckFatalWarnings(true)
67+
.withCheck(true)
68+
.withClang((dirs.bin / "clang").toPath())
69+
.withClangPP((dirs.bin / "clang++").toPath())
70+
// TODO: Make friendly for non-nix folks.
71+
.withCompileOptions(config.compileOptions :+ s"-I${dirs.includes}")
72+
.withEmbedResources(false)
73+
.withGC(GC.immix)
74+
.withIncrementalCompilation(true)
75+
// TODO: Make friendly for non-nix folks.
76+
.withLinkingOptions(config.linkingOptions :+ s"${dirs.lib}/liburing.a" :+ s"${dirs.lib}/libcrypto.a")
77+
.withLinkStubs(true)
78+
.withLTO(LTO.full)
79+
.withMode(Mode.releaseFast)
80+
.withOptimize(true)
81+
),
82+
libraryDependencies ++= List(
83+
"co.fs2" %%% "fs2-core" % versions.fs2,
84+
"co.fs2" %%% "fs2-io" % versions.fs2,
85+
"com.comcast" %%% "ip4s-core" % versions.ip4s,
86+
"org.http4s" %%% "http4s-core" % versions.http4s,
87+
"org.http4s" %%% "http4s-dsl" % versions.http4s,
88+
"org.http4s" %%% "http4s-ember-server" % versions.http4s,
89+
"org.http4s" %%% "http4s-server" % versions.http4s,
90+
"org.typelevel" %%% "cats-core" % versions.cats,
91+
"org.typelevel" %%% "cats-effect-kernel" % versions.catsEffect,
92+
"org.typelevel" %%% "cats-effect" % versions.catsEffect,
93+
"org.typelevel" %%% "log4cats-core" % versions.log4cats,
94+
),
95+
)

0 commit comments

Comments
 (0)