On this page
Modules first A package and a program that uses itnx init, nx add, nx fetch
Testing a package
A package from the wild
Rules worth knowing
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:
[package]
name = "words"
version = "0.1.0"
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!")
}
/// 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:
[package]
name = "app"
version = "0.1.0"
[dependencies]
words = { path = "../words" }
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
[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:
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 dependency's name in the manifest is the identifier programs import.
- The first package to claim a name wins across the whole dependency graph; version conflicts are not resolved automatically, since there are no version ranges to resolve, only tags.
nexium.tomlis a small TOML subset: tables, strings, inline tables, arrays of strings, comments.- A package may declare
artifact link { c_sources = [...] }for C it ships with (the GUI does), and the program's build compiles it in.
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.
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 thev;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
- A manifest says
words = { path = "../words" }. What does a program write to use it?import wordsimport ../wordsuse words
Answer
import words. The dependency's name in the manifest is the identifier programs import. - 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.lockpins the commit the tag resolved to - it fails
Answer
nothing, until you fetch again:
nexium.lockpins 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. - 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.