EK9 in Depth

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.

  1. 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/, @Test and ek9 -t. ↩