Packaging and Sharing Your Program
Testing as You Go in Volume 1 said that a library cannot be published without a minimum of coverage; how that is measured
belongs to Testing the Compiler Trusts. This chapter is about shipping. In Maven,
Cargo and npm, publishing is a separate tool with its own manifest, its own credentials and its own idea of a version. In EK9
the manifest is the defines package block you met in Organising a Growing Codebase, the version is a value in that block,
and the tool that tests, versions and packages is the compiler you already run. Everything below was run with the compiler's local
commands: -PV, -IV, -P and -I.
The package block is the manifest
Here is a small publishable package, a few greeting functions and a thin program, with the package block as the last part of the file:
defines module book.packaging.greeter defines function greeting() as pure -> name as String <- rtn as String: `Hello, ${name}` farewell() as pure -> name as String <- rtn as String: `Goodbye, ${name}` shouted() as pure -> text as String <- rtn as String: text.upperCase() + "!" whispered() as pure -> text as String <- rtn as String: `(${text.lowerCase()})` defines program Greet() -> name as String Stdout().println(greeting(name)) defines package publicAccess <- true version <- 1.0.0-0 description <- "Greetings, packaged for sharing" license <- "MIT" tags <- [ "example", "greeting" ] applyStandardIncludes <- true capabilities <- ["org.ek9.lang::Stdout"]
defines package takes no name: the module name is the package name. Each line is a <- declaration with a literal of the
field's own shape; an unknown field name, or the wrong shape (tags <- "tools"), is E06260. publicAccess says the package may be
published once a public repository exists, capabilities declares the system powers the code uses (a section below), and applyStandardIncludes
is the build setting from Organising a Growing Codebase. The version is a native Version literal, MAJOR.MINOR.PATCH-BUILD, and not a
string:
$Version: 1.0.0-0
-PV prints it. -IV increments a part of it and rewrites the source file; here is -IV patch run on a copy, and the
difference it made to the file:
30c30
< version <- 1.0.0-0
---
> version <- 1.0.1-0
A second -PV on the copy prints:
$Version: 1.0.1-0
The parts are major, minor, patch and build, as semantic versioning would have them (a breaking change to the public
interface is major, a new capability is minor, a fix is patch), and the build number moves on every build attempt. The version
lives in the source, so a version bump is a commit that a diff shows, and no second file can disagree with it.
-P: packaging is gated by tests
ek9 -P main.ek9 packages the project, and it refuses unless the tests pass with at least 80% coverage (E83001, ek9 -h coverage).1
Begin with a project that has no tests:
No @Test programs found.
ek9: tests failed (exit 11)
Package : Not packaged: tests failed or coverage below the required threshold
Add one test, for greeting only, and the tests pass but the project does not:
ℹ️ Found 1 test:
1 assert (unit tests with assertions)
Executing 1 test...
✅ PASS book.packaging.greeter.tests::GreetingTest [Assert] (1ms)
Summary: 1 passed, 0 failed (1 total)
Types: 1 assert
Duration: 1ms
❌ E83001: Code coverage 20.0% is below required threshold 80%
Methods: 20.0% (1/5)
Lines: 20.0% (3/15)
Uncovered items:
4 method(s)
• book.packaging.greeter::farewell (./main.ek9:10)
• book.packaging.greeter::shouted (./main.ek9:14)
• book.packaging.greeter::whispered (./main.ek9:18)
... and 1 more
Use -t4 or -t5 for detailed coverage report.
ek9: coverage below the required threshold (exit 12)
Package : Not packaged: tests failed or coverage below the required threshold
The compiler tells you what to cover next, naming the items it found uncovered; what is counted is the subject of Testing the Compiler Trusts.
With the four tests in dev/:
defines module book.packaging.greeter.tests references book.packaging.greeter::farewell book.packaging.greeter::greeting book.packaging.greeter::shouted book.packaging.greeter::whispered defines program @Test GreetingTest() assert greeting("Ada") == "Hello, Ada" @Test FarewellTest() assert farewell("Ada") == "Goodbye, Ada" @Test ShoutedTest() assert shouted("hello") == "HELLO!" @Test WhisperedTest() assert whispered("HELLO") == "(hello)"
-P runs them, passes, and writes two files:
ℹ️ Found 4 tests:
4 assert (unit tests with assertions)
Executing 4 tests...
✅ PASS book.packaging.greeter.tests::FarewellTest [Assert] (2ms)
✅ PASS book.packaging.greeter.tests::GreetingTest [Assert] (2ms)
✅ PASS book.packaging.greeter.tests::ShoutedTest [Assert] (2ms)
✅ PASS book.packaging.greeter.tests::WhisperedTest [Assert] (2ms)
Summary: 4 passed, 0 failed (4 total)
Types: 4 assert
Duration: 10ms
book.packaging.greeter-1.0.0-0.zip
book.packaging.greeter-1.0.0-0.zip.sha256
The package is an archive of the source. This is the second difference from Maven and npm, which publish a built artefact:
Archive: .ek9/book.packaging.greeter-1.0.0-0.zip
Length Date Time Name
--------- ---------- ----- ----
0 dev/
530 dev/greeterTests.ek9
804 main.ek9
229 .package.properties
--------- -------
1563 4 files
main.ek9, the tests in dev/, and a properties file the compiler made:
#Package Properties
#Tue Nov 10 22:15:49 GMT 2026
depsFingerPrint=78A39FC30DAB8BF221BC9EB8C2567BB2ABEF57E72BA1E4E29A87A23DB4142391
moduleName=book.packaging.greeter
package=true
programs=Greet
sourceFile=main.ek9
version=1.0.0-0
Whoever depends on your package compiles your source, with their compiler, under their checks, and a .sha256 file beside the
archive pins its content. The cost is a consumer who recompiles what they depend on; what it buys is that nothing a package ships
escapes the compiler's rules, and nothing in it is opaque.
-I: installing to your own library
-I does what -P does, then installs the archive into a local library, which is the folder ~/.ek9/lib unless it is
pointed elsewhere. The runs in this chapter were made with a private home and a private library folder, so nothing here touched
a real one. After -I the library holds the archive and its checksum:
book.packaging.greeter-1.0.0-0.zip
book.packaging.greeter-1.0.0-0.zip.sha256
A library on your own disk is enough to share between your own projects, which is what the section on dependencies does.
Declaring what a package may do
A package that other people will build on is, for them, a supply-chain risk: any package can open a socket or read an environment
variable, and npm, Cargo and Maven do not ask a package to declare it. EK9's publishable package states its capabilities, and the
compiler checks the package's own code against the list. This program reads an environment variable but declares only Stdout:
defines package publicAccess <- true version <- 1.0.0-0 description <- "Reads the environment without declaring it" license <- "MIT" applyStandardIncludes <- true capabilities <- ["org.ek9.lang::Stdout"]
EK9 : Error : E12010: 'EnvVars' on line 16 position 13: 'EnvVars' requires capability 'org.ek9.lang::EnvVars': type requires declared package capability
See: ek9 -h E12010
EnvVars is a gated type, and it is not on the list. The gated types include the standard streams, the environment,
Networking, HTTP, TCP, UDP, signals, files and clocks; ek9 -h E12010 has the list and names the grouped ones (declaring
TCP covers TCPConnection and its handlers, and Networking needs EnvVars as well). The fix is to declare the capability the
code uses, and the package block then reads as an honest statement of what the package does:
defines package publicAccess <- true version <- 1.0.0-0 description <- "Greets a name taken from the environment" license <- "MIT" applyStandardIncludes <- true capabilities <- ["org.ek9.lang::EnvVars", "org.ek9.lang::Stdout"]
It prints, with the variable unset:
nobody to greet
Enforcement starts only when a package declares all four of publicAccess, version, license and capabilities; with any missing,
the package is not entering the supply chain under review and is not checked. Dev code (tests, fixtures) is exempt, because it is
never published. Enforcement is per module, so a dependency that adds a capability does not break your build; the change shows in
its package block, and that is the change a reviewer should look at when upgrading. The limit is the one the compiler states: it
verifies that the package's own code uses nothing it did not declare, for the gated types it knows about.
Depending on another package
A dependency is a deps entry in the package block, naming a module and a version:
defines package version <- 0.1.0-0 description <- "Depends on a library nobody has installed" applyStandardIncludes <- true deps <- { "book.shapes.lib": "1.0.0-0" }
Where does book.shapes.lib come from? The resolver looks in the local library, and then it would look in a remote repository.
There is no remote repository yet, and the compiler says so, with the exit code a script can test for:
ek9: dependency resolution failed (exit 17)
Resolve : dependency 'book.shapes.lib' version '1.0.0-0' (declared at unresolvedDependency.ek9:9) cannot be resolved: it is not unpacked in '~/.ek9/lib/book.shapes.lib-1.0.0-0' nor zipped as '~/.ek9/lib/book.shapes.lib-1.0.0-0.zip', and there is no remote repository to fetch it from yet
That is exit 17, a dependency that could not be resolved, and the message names the version and the line that declared it.
So to use a library today you install it first. Here is a small library, book.shapes.lib, with two functions and two tests:
defines module book.shapes.lib defines constant CIRCLE_CONSTANT <- 3.14159 defines function circleArea() as pure -> radius as Float <- rtn as Float: CIRCLE_CONSTANT * radius * radius squareArea() as pure -> side as Float <- rtn as Float: side * side defines package publicAccess <- true version <- 1.0.0-0 description <- "Area of simple shapes" license <- "MIT" applyStandardIncludes <- true
Installing it runs its tests and puts it in the library:
ℹ️ Found 2 tests:
2 assert (unit tests with assertions)
Executing 2 tests...
✅ PASS book.shapes.lib.tests::CircleAreaTest [Assert] (2ms)
✅ PASS book.shapes.lib.tests::SquareAreaTest [Assert] (2ms)
Summary: 2 passed, 0 failed (2 total)
Types: 2 assert
Duration: 4ms
A consumer module declares deps <- {"book.shapes.lib": "1.0.0-0"}, adds a references block naming book.shapes.lib::circleArea
and book.shapes.lib::squareArea, and calls them. Building it resolves the dependency from the library and compiles the library's source with the consumer's. With -v the
compiler reports the dependency among the files it compiles:
Resolve : Checking 'book.shapes.lib-1.0.0-0'
Resolve : Already unpacked 'book.shapes.lib-1.0.0-0'
Resolve : Complete
Compile!: 1 dependency source file(s)
and the program runs:
circle r=2: 12.56636
square s=3: 9.0
-Dp resolves dependencies without building, and cleans first. Resolution is transitive, patch-compatible versions are promoted
to the later one, a major-version mismatch fails the build, and circular package dependencies are detected; ek9 -h package is
the reference for those rules.
What is not here yet
Two steps of the release workflow in ek9 -h package are not available to you today. -D deploys packaged software and would
trigger -P first and then -Gk, which generates the signing keys a deployed package carries; both belong to a public
repository, and there is none yet. They are planned, they are not shown in this chapter, and a package you build today stays
on your disk and the disks you copy it to. Nothing in the rest of the chapter depends on them.
The cost
A project is packaged only when its tests pass with at least 80% coverage (E83001, ek9 -h coverage). A package that exercises a gated type owes a
capabilities entry for each, and keeps it current as the code changes. The consumer builds your source, not a binary you
vouch for, so a dependency adds to their compile time. Sharing means handing over an archive.
What that buys is the sentence the book opened with: every package you can install has passed its own tests, states what it may touch, and arrives as source the compiler has checked as hard as the code you wrote yourself. A version bump is a diff, a dependency is a line in a block, and a supply-chain question has an answer in the file that declares the package.
-
Testing the Compiler Trusts is where coverage is measured and explained. A reader who skipped Volume 1 should read Testing as You Go, which introduces
dev/,@Testandek9 -t. ↩