Projects and packages¶
A single file is enough for a long time. When a program grows past it, or when
you want code somebody else wrote, turn it into a project: a directory with a
candela.toml in it that names the package, says which file to run, and lists
what it depends on.
Start a project¶
candela new writes two files:
candela.toml is the manifest. src/main.cdl is the entry point, the file
candela run compiles and runs when you do not name one.
Every key is listed in the manifest reference.
Run, check, build¶
Inside a project, three verbs take the entry point from the manifest:
candela run # compile and run
candela check # compile and stop, reporting any error
candela build # compile to a .cdlb artifact
Each also takes a file, so candela run tools/report.cdl runs that file in the
project instead. Arguments after either reach the program through argv():
They work from anywhere inside the project. candela looks for candela.toml in
the current directory and then in each directory above it, so a command typed
in src/ finds the project it belongs to.
A library needs no main. run and build want one, because both start the
program there, and check compiles a file that has none, so a package whose
entry only declares functions for other projects to import checks cleanly.
Add a dependency¶
That asks the registry for the newest shapes, downloads it, records it in the
manifest, and writes the lock file:
The requirement is the version that was resolved, at its major and minor, so a later fetch takes patch releases and nothing wider. Ask for a different one by naming it:
candela add shapes@^2
candela add [email protected]
candela remove shapes takes it out again.
Import from a package¶
A package is another place a library import reads from. Its own name reaches
its entry point, the file its manifest names (src/main.cdl unless it says
otherwise), and a path inside it reaches the files beside that entry:
import "shapes" as shapes; // the package's src/main.cdl
import "shapes/circle" as circle; // src/circle.cdl inside the package
fn main() {
print(shapes::area(3, 4));
}
Nothing else about importing changes: the same one form, the same as, the
same bare-import merge. See modules.
The lock file¶
candela.lock sits beside the manifest and records the exact version of every
package the project resolved to, direct and indirect. Commit it. It is what
makes two machines, and your machine tomorrow, compile the same code.
candela fetch # resolve and download what the manifest lists
candela fetch --locked # fail rather than change candela.lock
candela update # move to newer versions the manifest still allows
candela update shapes # move just that one
--locked is the one to use in CI: it turns a lock file that disagrees with
the manifest into an error instead of a quiet rewrite.
Add --offline to run, check, build or fetch to resolve from packages
already downloaded and never reach the network.
The registry client¶
Resolution is done by lpm, a separate program that talks to the registry.
candela runs it and reads the answer; it does not resolve versions itself.
The toolchain installer puts lpm at ~/.local/bin/lpm, and candela downloads
it there itself the first time a project needs it and finds none. You do not
have to run it by hand.
To run one of your own instead, set LPM_BIN to it. That is an override:
candela runs what it names and looks nowhere else, so if the binary cannot do
the job candela says so and names it rather than reaching for another client.
Publish a package¶
A package is a project with something worth importing in it. Give it a
description, and put the functions you want reachable in the entry point: that
file is what import "name" reads, and the files beside it are what
import "name/file" reads.
The archive a release serves has to live somewhere. The reusable workflow builds it, uploads it to a GitHub release, and tells the registry where it is, so pushing a tag is the whole of publishing:
# .github/workflows/release.yml
name: release
on:
push:
tags: ["v*"]
permissions:
contents: write
jobs:
release:
uses: lumen-fx/candela/.github/workflows/build-package.yml@main
with:
candela-version: latest
secrets:
lpm-token: ${{ secrets.LPM_TOKEN }}
The workflow installs candela, runs candela check on the package, packs the
archive, publishes the release, and calls the registry with the manifest's
version and dependencies. The contents: write grant is what lets it create
the release; a workflow that only wants the check and the pack, on a pull
request, grants contents: read and stops there.
The archive carries the manifest and the directory the manifest's entry sits
in, which with the default entry is candela.toml and src/. An entry
elsewhere moves the directory with it, so a package entered at lib/shapes.cdl
ships lib/, and one whose entry is at the package root ships everything
beside the manifest apart from dist/, target/, any .tar.gz, .tgz or
.zip lying there, and every name that starts with a dot. A dist/ directory
ships whenever the package has one.
candela-version takes latest, a version such as 0.0.6 or v0.0.6, or
nightly for the rolling prerelease. Each of them installs on every target the
workflow packs for, Windows included.
The release also records which candela a user needs. Name the oldest one the package compiles on in the manifest, and the workflow records that:
A package that names none records the version of the toolchain that checked it, and says so in the log. On the nightly channel that version has no release yet, so a package published from nightly without the key is installable only by nightly users until it ships.
A package that ships a native library builds one archive per desktop target.
Name the command that builds them, leaving the results in dist/:
To publish an archive you uploaded yourself, name it:
A candela project cannot depend on a Lumen package. They are different platforms, and a dependency on one is refused by name.
Next¶
The manifest reference lists every key, and the CLI reference lists every verb and flag.