Packaging in EK9

This section covers the package construct.

The package construct is a flexible, gradual way to control what your programs consist of. It is a little like pom.xml in maven or package.json in npm, but builds the project model up more gradually. You don't need to know either of those: both package your software in some form and pull in the other packages you need.

The problem we are attempting to solve

If you only do small scale local development for yourself, you don't need packages at all. They solve problems that appear as software grows.

What makes up your whole program

The first issue a package solves is describing which source files to compile. You can put all your source in one file and compile that, and the examples show how far that goes. In time you will want to break the code up into several files, ideally with different module name spaces to give it more structure. How far to go is a matter of taste, practice, experience and tooling; it is easy to go from one extreme to the other.

Which files to compile

So, first issue: which files need compiling?

You may start with a 'grand plan' and fill in the blanks, or start coding and see where it takes you; EK9 supports either. The package has been developed in stages to support the second, fluid approach: prototypes, refactoring, and unit tests written as you go. Design note: start with code, then refactor explains why.

By default the EK9 compiler compiles only the single file you give it on the command line. Normally that file has at least one program you want to run, but not always.

When more files are needed, the package construct becomes useful: it lets you 'list' the files to compile, as shown below.

⚠ Important — what "single file" actually means

The CLI does not auto-discover sibling .ek9 files. If you run ek9 helloWorld.ek9 and the same directory also holds compA.ek9, traits.ek9 and so on, those are not compiled; only helloWorld.ek9 is. Its uses of symbols from the sibling files are reported as not resolved (E50001), as Step 2 below shows.

To compile multiple files you have two options:

  1. Put the defines package block in the file containing the program(s) you intend to run. Add defines package / applyStandardIncludes <- true to helloWorld.ek9 itself, then run ek9 helloWorld.ek9. Use this when the project is built and run as a whole.
  2. Use the -cd / -Cd dev-build flags (for example ek9 -cd helloWorld.ek9), which discover the full source tree whatever the package declaration says. They compile but do not run, and they also add debug instrumentation and the dev/ code, so they are not for release builds.

Important — a separate package.ek9 file does not work for running. It is tempting to put the defines package block in its own file and run ek9 -r MyProgram package.ek9. That fails with does not contain any programs: the CLI looks for the named program in the file you handed it. Keep the package block in the same file as the program(s) you want to run. A separate package file is fine for compile-only operations such as ek9 -P (packaging) or ek9 -Dp (dependency resolution).

EK9 IDE users: the IDE builds in dev-build mode, so its workspace shows every .ek9 source and compiles across files. To run a program it hands the CLI the program's own source file, so that file must contain a defines package block (or the program must be self-contained). Its Package, Deploy and Resolve-Dependencies operations use the project's package file directly, because they don't look up a program.

Compiling a single file by default is the first tier of EK9's graduated project model, in which complexity scales with the project:

  1. One-off script — a single .ek9 file with one or two functions and a program. ek9 my-script.ek9 runs it. No directory layout, no package construct, no ceremony.
  2. Small project — a dedicated directory holding several related .ek9 files, all in the same module. A defines package block (usually in the program's main file, or in a dedicated package.ek9 for compile-only use) brings in the siblings through includeFiles or applyStandardIncludes.
  3. Multi-module system — a project tree with sub-directories, several defines module namespaces, perhaps external deps, and a package block that declares version / license / capabilities for publication.

As soon as your program needs siblings you move to tier two by adding the package construct, and from then on the package is the project model.

A worked example: from "Hello World" to multi-module

Here is a small project growing from one file to three files across two modules:

Step 1 — One-off script (tier 1). Single file, single module, one program:

#!ek9
defines module site.packaging.single

  defines program
    HelloWorld()
      stdout <- Stdout()
      stdout.println("Hello, World")
Hello, World

Save it as packaging_single.ek9 and run it with ek9 packaging_single.ek9. No package and no directory layout.

Step 2 — Add some shared code. The project has grown, and some records and traits are worth factoring out into packaging_nopackage_traits.ek9, in the same module:

#!ek9
defines module site.packaging.nopackage

  defines record
    BaseRecord as open
      anything as Integer: 90

      default operator ?

  defines trait
    Quack
      quack() as pure
        <- rtn as String?

  defines class
    Duck with trait of Quack
      override quack() as pure
        <- rtn as String: "Quack"

The program file, packaging_nopackage_hello.ek9, now uses BaseRecord and Duck from the other file:

#!ek9
defines module site.packaging.nopackage

  defines program
    HelloWorld()
      stdout <- Stdout()
      record <- BaseRecord()
      duck <- Duck()
      stdout.println(`Hello, World ${record.anything} ${duck.quack()}`)

The CLI cannot compile this with ek9 packaging_nopackage_hello.ek9: the CLI is still in single-file mode, so BaseRecord and Duck don't resolve:

$ ek9 -c packaging_nopackage_hello.ek9
ek9: compilation failed (exit 8)
EK9     : FULL_RESOLUTION: Errors in ./packaging_nopackage_hello.ek9
EK9     : Error   : E50001: 'BaseRecord' on line 7 position 16: not resolved
             See: ek9 -h E50001
EK9     : Error   : E50001: 'Duck' on line 8 position 14: not resolved
             See: ek9 -h E50001
EK9     : Compilation failed

Time to move to tier 2.

Step 3 — Tier 2: add a package. The package block goes inside the file containing the program, so the same command, ek9 packaging_withpackage_hello.ek9, still runs it:

#!ek9
defines module site.packaging.withpackage

  defines package
    version <- 1.0.0-0
    description <- "Simple samples"
    applyStandardIncludes <- true

  defines program
    HelloWorld()
      stdout <- Stdout()
      record <- BaseRecord()
      duck <- Duck()
      stdout.println(`Hello, World ${record.anything} ${duck.quack()}`)

The other file is unchanged (only its module is this project's):

#!ek9
defines module site.packaging.withpackage

  defines record
    BaseRecord as open
      anything as Integer: 90

      default operator ?

  defines trait
    Quack
      quack() as pure
        <- rtn as String?

  defines class
    Duck with trait of Quack
      override quack() as pure
        <- rtn as String: "Quack"
Hello, World 90 Quack

With the package block in packaging_withpackage_hello.ek9, ek9 packaging_withpackage_hello.ek9 compiles every .ek9 file in the directory tree (excluding dev/ for non-dev builds) and runs HelloWorld. The only addition was the four-line package block at the top of the program file.

You could put the package block in a separate package.ek9 file and use it as the target for compile-only operations such as ek9 -P package.ek9 (packaging) or ek9 -Dp package.ek9 (resolve dependencies). Running needs the package block beside the program, as the note above explains.

Convention — keep programs with the package. If your project has several defines program blocks (a small CLI tool with several subcommands, say), put them all in the file with the defines package block. Then ek9 helloWorld.ek9 compiles the whole tree and ek9 -r OtherProgram helloWorld.ek9 finds any program in the project, with no hunting for the file that hosts an entry point. The project then has:

It is the same pattern as a single Main.java in a Java project or main.go in a Go module: the entry point is one well-known file and everything else is library code. EK9's per-file run rule reinforces the convention.

Step 4 — Tier 3: a second module. A richer example goes into a different module:

#!ek9
defines module site.packaging.twomodules.introduction

  defines class
    AnotherClass
      greeting() as pure
        <- rtn as String: "from another module"

The program's module references the class it uses:

references
  site.packaging.twomodules.introduction::AnotherClass
Hello, World from another module

With applyStandardIncludes <- true already in place, the new file is picked up with no package edit, and references handles the cross-module visibility. The package still describes a single distributable unit (one version number, one license) even though the source spans several defines module blocks.

Why EK9 does it this way. At no point did we rewrite earlier code or rename anything. Each tier added one concept (multiple files, then a package, then a second module) without disturbing what came before. The package construct is purely additive, and stays out of your way until your project needs it.

Define the list of files to be compiled

Say the content below is in a file called packaging_includes.ek9. It works rather like a 'Makefile' in 'C' and 'C++':

#!ek9
defines module site.packaging.includes

  references
    site.packaging.includes.maths::PI
    site.packaging.includes.physics::SPEED_OF_LIGHT

  defines package

    version <- 1.0.0-0
    description <- "Example of different constants in different modules."

    //We only want this file and these two to be compiled.
    includeFiles <- [
      "packaging_includes_maths.ek9",
      "packaging_includes_physics.ek9"
      ]

  defines program
    ShowConstants()
      stdout <- Stdout()
      stdout.println(`Pi is ${PI} and the speed of light is ${SPEED_OF_LIGHT}`)

The two files it names each define a constant in a module of their own:

#!ek9
defines module site.packaging.includes.maths

  defines constant
    PI <- 3.14159
#!ek9
defines module site.packaging.includes.physics

  defines constant
    SPEED_OF_LIGHT <- 299792458
Pi is 3.14159 and the speed of light is 299792458

This suits a project with a few source files. It lets you take a one-file application and, over time, add features and split them across a small number of related files. Beyond a handful of files, remembering to add each new one becomes a burden.

It is aimed at small utility programs and tools that are a bit too big for a single source file, perhaps with some constructs shared by several small utilities.

Include all EK9 source files

Once your project reaches a certain size it has a directory of its own (small separate applications may have lived together, sharing little). You want all the related source code in that directory, with subdirectories for code that belongs together.

You can now stop listing the source files in includeFiles and write:

defines package
  version <- 1.0.0-0
  description <- "Simple samples"
  applyStandardIncludes <- true

This one directive includes every file with the .ek9 suffix in the compilation: in the directory of the source file holding the package directive, and in all its subdirectories except /dev, which is included only in a development build. The build types are described later.

Why EK9 does it this way. Why not make this the default? Because of 'different use cases'. Sometimes you want a quick utility in a single source file: a program and a couple of functions, with no package to set up. EK9 tries to make easy things 'easy' and hard things possible and 'fairly easy'; see EK9 purpose.

This 'directory' approach, with special named subdirectories, is closer to 'maven' or 'gradle'. So far your code uses only the standard libraries shipped with EK9 and has no external 'dependencies'. Those come next.

What about libraries

The second issue: you won't want or need to write all the code for your application yourself, or even with your team. Someone else may well have written code you would find useful, and made it available in the EK9 repository.

Equally, you may have developed constructs you want to package as a reusable library and make available within your own group or company, or publicly through the EK9 repository.

Either way, update your package directive to state the libraries (dependencies) your project needs.

Developers who want you to use their packages provide sample code in the /dev directory. Look in your library $HOME/.ek9/lib/{package-name}/dev for examples of how the developer intended the package to be used: expect tests and examples there, with documentation in the source code, to help you get started with the library.

Defining dependencies
defines module site.packaging.deps

  defines package

    version <- 1.0.0-0
    description <- "Example of different constants in different modules."

    //No need for includeFiles for source code anymore.
    applyStandardIncludes <- true

    deps <- {
      "ekopen.network.support.utils": "1.6.1-9",
      "ekopen.net.handy.tools": "3.2.1-0"
      }

This project declares two dependencies, each at a specific version. EK9 also brings in any transitive dependencies, and the integrated build system detects circular dependencies between modules. EK9 is very strict on both dependency naming and version numbering, described below.

Dependencies and Libraries

To use or publish packages of constructs, you have to address version numbering and dependency naming.

Dependency Naming and Version Numbers

The EK9 repository has two main spaces for deploying modules.

Wild West

Naming public modules in the 'ekopen' space is a free for all: first come, first served. Corporates get no special treatment over freelance developers; there are no legal cases and no complaints, and whoever gets there first gets the name space. Names must have at least four parts, e.g. ekopen.network.support.utils. The developer who issues the first package to ekopen.network.support then controls all the final module names in that space: they can publish and deploy as many modules under it as they like, and only they can.

Civilisation

Here public module names are linked to a controlled DNS domain, so individuals or corporate entities can publish packages under their own name. Only the entity that controls the ek9lang.org domain can publish a package like org.ek9lang.core.functions. Domain validation uses the standard mechanism: the developer creates a suitable TXT DNS entry, the EK9 repository checks it, and the developer can then publish to that name space.

In Summary

If you are or represent a corporate entity, stay in Civilisation and keep control of your corporate brand, package and module names. If you like the free for all of the Wild West, don't complain about naming clashes; that's what it is like. If you don't like it, register a DNS domain and use Civilisation.

Version Numbering

Version numbers are the third issue. EK9 uses a strict form of Semantic Versioning:

No other variations are supported. Features explains why you might want a feature name in your version number.

The Public Interface

All constructs defined in the packaged module name are the public interface. In the example above, if class CRC and function reverse were defined in module ekopen.net.handy.tools, they would be its public interface.

To keep the same major version number, you cannot alter these constructs or their parameters in any way. No non public interface constructs can be used as parameters into or out of the public interface, and any transitive dependencies the module pulls in must also stay major version compatible.

Why EK9 does it this way. This may seem strict or draconian, but it is necessary for dependency consistency. A developer needs to know that version 4.5.6-21 can be used in place of 4.4.1-9: the public code interface is the same, and the transitive dependencies remain major version compatible. The EK9 compiler enforces this by failing the build if dependency resolution would need a dependency with a higher major version than the one specified, since that indicates a breaking API change.

In summary:

Dependency Exclusions

The fourth issue: a transitive dependency is pulled in and has known defects, and you need to avoid that buggy version. This directive excludes a dependency when it has been pulled in:

defines module site.packaging.excludes

  defines package

    version <- 1.0.0-0
    description <- "Example of different constants in different modules."

    //No need for includeFiles for source code anymore.
    applyStandardIncludes <- true

    deps <- {
      "ekopen.network.support.utils": "1.6.1-9",
      "ekopen.net.handy.tools": "3.2.1-0"
      }

    excludeDeps <- {
      "ekopen.some.bad.dependency.pack": "ekopen.org.supertools.util"
      }

The directive is a dictionary (map) of dependency names. It means: "exclude dependency 'ekopen.some.bad.dependency.pack' when it has been pulled in as a transitive dependency from 'ekopen.org.supertools.util'". It need not be a direct transitive dependency; it can be a dependency of a dependency, and so on.

Version Numbers for the module and program you are developing

Once your software is more than tooling for yourself and others use it, give it a version number. Your users will want to know when's the next release, and whether version x.y.z will have this feature or that, so versions are critical for managing releases, defects and improvements. Add the version directive to the package declaration, and align it with a source code repository such as git.

Here the version is 2.3.14-0. Each time you build, patch, or make minor or major improvements, the release number must change; the EK9 compiler can do that for you.

defines module site.packaging.version

  defines package

    version <- 2.3.14-0

    description <- "Example of different constants in different modules."

    //No need for includeFiles for source code anymore.
    applyStandardIncludes <- true

    deps <- {
      "ekopen.network.support.utils": "1.6.1-9",
      "ekopen.net.handy.tools": "3.2.1-0"
      }

    excludeDeps <- {
      "ekopen.some.bad.dependency.pack": "ekopen.org.supertools.util"
      }

Once a version number is in place, the EK9 compiler can alter it as part of a build pipeline. When you supply software to others you need a reliable, repeatable and controlled build mechanism, not a build on your local PC.

With GitHub Actions, AWS, Azure or Hudson/Jenkins you can define the phases of a build, including changes to the version being built. The command line lists the options for updating the version number.

Publishing a module

Some of your code, a range of functions, classes or other constructs, may be useful to other people. It may already be in a git repository with contributions from other developers. Now you want to make specific versions available through the EK9 repository.

First decide Wild West or Civilisation and name your module accordingly, so that it is unique. Then add a few more directives.

The module below is named site.packaging.publish, as every compiled example on this site is; in a real project it would be named for its space, for example ekopen.math.simple.constants.

defines module site.packaging.publish

  defines package

    publicAccess <- true

    version <- 2.3.14-0

    description <- "Example of different constants in different modules."

    tags <- [
      "mathematics",
      "constants"
      ]

    license <- "MIT"

    //Pure computation - no system resources used. No 'capabilities' block is
    //declared, so capability enforcement is not activated for this package.
    //See the Capability Security section below for details.

    applyStandardIncludes <- true

    includeFiles <- [
      "License.txt",
      "Copyright.txt",
      "Authors.txt"
      ]

    //certain files like .gitignore we don't want to package
    applyStandardExcludes <- true

    deps <- {
      "ekopen.network.support.utils": "1.6.1-9",
      "ekopen.net.handy.tools": "3.2.1-0"
      }

    excludeDeps <- {
      "ekopen.some.bad.dependency.pack": "ekopen.org.supertools.util"
      }

Renamed to ekopen.math.simple.constants (if ekopen.math.simple is still available), the software can be packaged and uploaded to the EK9 repository. As the owners of ekopen.math.simple you can publish any number of packages under that structure, and no other developers can publish with that module naming. The same holds for a domain name you control: only your team can publish to that module name.

Importantly, once a version of a module is published it cannot be retracted, deleted or altered (other than by publishing a new version). Others may now reference it, and retracting it would break their builds.

How to publish

Publishing to an EK9 public repository is not instant. The repository takes several steps to make sure the code is safe and meets basic standards: the content is scanned for viruses and checked against minimum standards (size, acceptable language), and the EK9 server compiles it to check that it compiles.

Capability Security: What Your Package Is Allowed to Do

Once you move from local development to publishing a module that other people depend on, you can add one more declaration inside defines package: the list of system capabilities your code uses. This is EK9's compile-time defence against supply chain attacks. The deploy command does not require it (it needs only publicAccess, version and license), but while the list is present the compiler holds the module's own code to it.

In npm, PyPI, Maven Central and crates.io, a dependency is free to open network connections, read your environment variables or write to your filesystem without telling anyone. A JSON parser and a credential-stealing trojan look identical in their published metadata, and supply chain attacks exploit exactly this gap. In EK9 a package can declare the system resources it uses, and while that declaration is present the compiler checks the module's own code against it.

When capability declarations are required

Capability enforcement applies to a package — which must then declare everything it uses — when all four of these are in its package block:

When any of the four is missing, enforcement is off and you can use any EK9 type. That is the case when you use the package block only for file organisation (applyStandardIncludes) or for dependency management (deps) and are not yet preparing to publish. Experimenting on your own machine carries no supply chain risk, so it carries no supply chain obligation.

The capabilities block itself says "I am entering the supply chain and accept the capability-review obligations." A published package that pre-dates the capability feature compiles unchanged until its author adds a capabilities block; there is no forced migration, and each module opts in when it is ready.

Test code under a dev/ directory is exempt. Tests need stdio, the filesystem and other system resources, and test code is never consumed by other packages, so it is not part of the supply chain.

Gated types: what you must declare

Gated types are built-in types that give access to system resources. Using one in a package under capability enforcement without declaring its capability is a compile error (E12010). The gated types are:

Declare each gated type in the capabilities list by its fully-qualified name, with the module path org.ek9.lang::. Where a gated type has related helper types (declaring TCP permits TCPConnection and TCPHandler), declare only the primary type. The rich error message for E12010 lists the groupings.

Declaring capabilities in your package

The capabilities block sits inside defines package with version, license and deps. For a publishable package that reads environment variables and makes HTTP calls:

defines module site.packaging.httpclient

  defines package

    publicAccess <- true
    version <- 1.0.0-0
    description <- "HTTP client library"
    license <- "MIT"

    capabilities <- [
      "org.ek9.lang::TCP",
      "org.ek9.lang::HTTPRequest",
      "org.ek9.lang::HTTPResponse",
      "org.ek9.lang::EnvVars"
      ]

    deps <- {
      "org.ek9.collections": "1.0.0-0"
      }

For a pure-computation package — a JSON parser, a string-manipulation library, a data-structure collection, a mathematical package — omit the capabilities block. Without it the fourth condition is not met, enforcement is off for the module, and the package compiles as it did before the capability feature. This is the recommended pattern for a library that touches no system resources:

defines module site.packaging.jsonparser

  defines package

    publicAccess <- true
    version <- 2.1.0-0
    description <- "Pure JSON parsing library"
    license <- "MIT"

    //No 'capabilities' block: pure computation, no gated types used.
    //Omitting the block is the current idiom for pure libraries.

    deps <- {
      "org.ek9.collections": "1.0.0-0"
      }

Add a capabilities block, even with one entry, and the module comes under enforcement: every gated type it uses must be covered. You decide when a module moves from "unenforced" to "capability-governed", and no package that pre-dates the feature breaks.

Capability enforcement is per-module

Each package declares its own capabilities, and the compiler checks only that package's own code against them. Depending on another package does not inherit its capabilities, and a dependency that adds a capability does not cause compile errors up the dependency chain. Your module is responsible only for what your own code does.

Why EK9 does it this way. This is a deliberate simplification. The compiler's job ends at the module boundary: "do this module's declared capabilities cover its own gated type usage?" is the only question it answers. What your dependencies declare is a matter for the repository and your review workflow. The package block of a new dependency version is plain EK9 source, always legible, and the EK9 repository tooling is designed to show capability changes between versions so a person can review them at upgrade time.

So if a library you depend on adds a capability in v2, your build does not break. The repository's version-diff view highlights the change, and you decide whether to upgrade. This trades a strict compile-time tripwire for a simpler compiler and a review workflow that fits how developers evaluate dependency updates.

Why this is worth the extra lines

The cost is a few extra lines in your package block, and an occasional compile error when you start using a new gated type. The benefit: while a capabilities block is present, every gated type the module's own code uses must be in it, so network access, your secrets or your filesystem show up in the package block.

A package with no capabilities block is not checked at all: it can use any gated type, and nothing about it appears in its package block. The compiler does not force a package to keep the block either. So when you review a dependency update, look first at whether it has a capabilities block; if it does, one that has been added, changed or removed is the change to check. For a dependency that carries the block, that is the difference between "the new version now declares TCP, why?" and noticing weeks later that data was being exfiltrated.

Many packages need no capabilities at all. When a dependency's capabilities block gains org.ek9.lang::TCP or org.ek9.lang::EnvVars where it had none before, that is the change to look at. You write the capability lines once per package, and the compiler checks them for as long as they are there.

The full design, the rationale, and detailed attack scenarios with their mitigations are in EK9_PACKAGE_CAPABILITY_SECURITY.md.

Summary

You can progress from small programs in single EK9 source files to medium, large and very large suites of software. They start small, gain dependencies, and can themselves be packaged and published. Each step adds a little more detail to the package construct as your applications and projects grow.

Final Steps!

The final section is on the command line compiler tool.