Packages

A package is a directory with a nexium.toml and a src/ folder of modules. A program depends on one by path or by git tag, and imports its modules like any other. There is no registry and no build script: a dependency is source code the compiler reads.

Modules first#

Every .nx file is a module. import foo loads foo.nx next to the file that says so, and its pub items are reached as foo.item. That is how the GUI chapter found gui/nexium_gui.nx from gui/counter.nx, and it is enough for a program that is a handful of files in one directory.

A package is the same idea with a boundary around it, so that a directory of modules can be used by programs elsewhere without knowing its layout.

A package and a program that uses it#

The book's example is two directories:

topo/code/pkg/
  words/
    nexium.toml
    src/
      lib.nx        # `import words`
      cases.nx      # `import words.cases`
  app/
    nexium.toml     # depends on ../words
    main.nx

The package:

topo/code/pkg/words/nexium.toml
[package]
name = "words"
version = "0.1.0"
topo/code/pkg/words/src/lib.nx
import cases

/// The library's face: what `import words` sees.
pub fn shout(s: []u8) -> String {
    var out = cases.upper(s)
    out.append("!")
    return out
}

test "shout" {
    expect_eq(shout("go")[..], "GO!")
}
topo/code/pkg/words/src/cases.nx
/// A second module of the package, reached from the app as `words.cases`.
pub fn upper(s: []u8) -> String {
    var out = String.new()
    for c in s { out.push_byte(if c >= 'a' and c <= 'z' { c - 32 } else { c }) }
    return out
}

pub fn lower(s: []u8) -> String {
    var out = String.new()
    for c in s { out.push_byte(if c >= 'A' and c <= 'Z' { c + 32 } else { c }) }
    return out
}

Inside the package, import cases refers to the package's own src/cases.nx, never to a file of the program that uses it, so two packages may both have a module called util. src/lib.nx is what import words means from outside; the other modules are reached as words.cases. Only pub items cross the boundary.

The program:

topo/code/pkg/app/nexium.toml
[package]
name = "app"
version = "0.1.0"

[dependencies]
words = { path = "../words" }
topo/code/pkg/app/main.nx
import words
import words.cases

fn main() {
    println("{}", .{words.shout("summit")})
    println("{}", .{cases.lower("QUIET")})
}
$ cd topo/code/pkg/app
$ nx run main.nx
SUMMIT!
quiet

The compiler finds nexium.toml by walking up from the root source file, reads the [dependencies] table, and resolves import words through it.

nx init, nx add, nx fetch#

nx init                                    # writes nexium.toml (and main.nx in an empty folder)
nx add words --path ../words               # a directory on disk
nx add greet --git https://github.com/someone/greet --tag v1.2.0
nx fetch                                   # clone the git dependencies, write nexium.lock

nx add records the dependency; nx fetch clones every git dependency, transitively, into nexium_modules/ next to the manifest with a shallow checkout of the tag, and writes nexium.lock with the exact commit each one resolved to. Commit the lock file; commit nexium_modules/ too if you want builds with no network (vendoring), or ignore it and fetch after cloning.

To publish a package: push the repository and tag it. Users depend on the tag; the lock file pins the commit, so a moved tag changes nobody's build until they fetch again.

Testing a package#

A package's tests are test blocks in its modules, run from its own directory:

$ cd topo/code/pkg/words
$ nx test src/lib.nx
ok    shout

1 passed, 0 failed

A package from the wild#

The first package outside this repository is a Discord Rich Presence SDK, the nexium/ directory of statusmith. It lives beside the app it belongs to, so the dependency names the directory with dir; the lock still pins the repository's commit.

nx add discord_rpc --git https://github.com/Londopy/statusmith --tag sdk-v0.1.0 --dir nexium
topo/code/discord/nexium.toml
[package]
name = "card"
version = "0.1.0"

[dependencies]
discord_rpc = { git = "https://github.com/Londopy/statusmith", tag = "sdk-v0.1.0", dir = "nexium" }

A card on your profile is a few lines, and the program says what the card would have said when there is no Discord to talk to:

topo/code/discord/main.nx
import discord_rpc

fn main() {
    var a = discord_rpc.activity()
    a.details = String.from("climbing the Topo")
    a.state = String.from("chapter 19")
    a.start_ms = time.now()
    // your Application ID from the Discord Developer Portal goes here
    var client = discord_rpc.connect("1234567890123456789") catch {
        println("no Discord to talk to here; the card would have said: {} / {}", .{a.details, a.state})
        return
    }
    let headline = client.set_activity(&a) catch {
        println("Discord did not take the card", .{})
        return
    }
    println("on your profile now: Playing {}", .{headline})
    client.close()
}

The suite runs this one when it is online (NX_ONLINE=1, as CI is) and on Windows, where the SDK's pipe lives today; the package comes from GitHub. The Discord page has the fields of a card.

Rules worth knowing#

The packages document has the details.

Next: shipping a library.

Exercises#

The compiler grades these: nx topo 19 writes them into a directory, nx topo check runs them. Or do them here: the code is editable, Run runs it with the compiler in the page (it loads on the first run and never sends your code anywhere), Check grades it as nx topo check would, and Reset puts the exercise back. Ctrl+Enter runs.

write_newerwrite it

A package is published by tagging it. Fill in newer so the tests pass: whether tag a

is a later version than tag b, comparing major, minor and patch as numbers.

write_newer.nx
import std.strings

fn parts(tag: []u8) -> List(u32) {
    let t = strings.strip_prefix(tag, "v") orelse tag
    var out = List(u32).new()
    for p in t.split(".") { out.append(p.parse_int(u32) catch { 0 }) }
    return out
}

fn newer(a: []u8, b: []u8) -> bool {
    ???
}

fn main() {
    println("{} {}", .{newer("v1.10.0", "v1.9.3"), newer("v1.2.0", "v1.2.1")})
}

test "a later minor" { expect(newer("v1.10.0", "v1.9.3")) }
test "an earlier patch" { expect(!newer("v1.2.0", "v1.2.1")) }
test "the same version" { expect(!newer("v1.2.1", "v1.2.1")) }
test "a later major beats everything" { expect(newer("v2.0.0", "v1.99.99")) }
test "without the v" { expect(newer("0.10.1", "0.9.0")) }
The recorded output
true false
Hints
  • strings.strip_prefix(tag, "v") drops the v; split(".") gives the three parts
  • compare as numbers: "1.10.0" is newer than "1.9.3", though "1" < "9" as text
Solution
import std.strings

fn parts(tag: []u8) -> List(u32) {
    let t = strings.strip_prefix(tag, "v") orelse tag
    var out = List(u32).new()
    for p in t.split(".") { out.append(p.parse_int(u32) catch { 0 }) }
    return out
}

fn newer(a: []u8, b: []u8) -> bool {
    let x = parts(a)
    let y = parts(b)
    for i in 0..3 {
        let p = if i < x.len { x[i] } else { 0 }
        let q = if i < y.len { y[i] } else { 0 }
        if p != q { return p > q }
    }
    return false
}

fn main() {
    println("{} {}", .{newer("v1.10.0", "v1.9.3"), newer("v1.2.0", "v1.2.1")})
}

test "a later minor" { expect(newer("v1.10.0", "v1.9.3")) }
test "an earlier patch" { expect(!newer("v1.2.0", "v1.2.1")) }
test "the same version" { expect(!newer("v1.2.1", "v1.2.1")) }
test "a later major beats everything" { expect(newer("v2.0.0", "v1.99.99")) }
test "without the v" { expect(newer("0.10.1", "0.9.0")) }

Quizno peeking

  1. A manifest says words = { path = "../words" }. What does a program write to use it?
    • import words
    • import ../words
    • use words
    Answer

    import words. The dependency's name in the manifest is the identifier programs import.

  2. A tag you depend on is moved to a new commit. What happens to your build?
    • it changes at once
    • nothing, until you fetch again: nexium.lock pins the commit the tag resolved to
    • it fails
    Answer

    nothing, until you fetch again: nexium.lock pins the commit the tag resolved to. The lock file records the exact commit; commit it, and a moved tag changes nobody's build until they choose to.

  3. Two packages in the dependency graph are both called util. Which one is used?
    • the newer one
    • the first to claim the name, across the whole graph
    • the build fails
    Answer

    the first to claim the name, across the whole graph. There are no version ranges to resolve, only tags, so the first claim wins; the packages document says how to rename one.