Reference

Opal

Opal, OSL's package manager and project file. The opal.json fields, every command, Git and Go dependencies, scripts and lint settings.

Opal manages an OSL project: the opal.json file that describes it, the Git and Go packages it depends on, the scripts you run during development, and the lint settings that tune the compiler's warnings. It installs with OSL as the opal command. osl opal <command> runs the same code, so everything on this page works either way.

A project#

A project is a folder with an opal.json in it. Make one in the current folder:

Terminal
mkdir my-project
cd my-project
opal init

opal init [name] writes opal.json and adds .opal/ to .gitignore. The name defaults to the folder's name. It does not create or enter a folder, and it refuses to overwrite an existing opal.json. Every other command looks for opal.json in the current folder and then in each parent, so you can run them from any subfolder.

PathPurposeCommit it?
opal.jsonMetadata, dependencies, scripts, commands and lint settingsYes
opal.lockThe exact Git commits and Go module versions in useYes
.opal/Checked-out packages and built commandsNo

opal.json#

{
  "name": "mist/example",
  "version": "1.0.0",
  "description": "A small example",
  "main": "main.osl",
  "dependencies": {
    "git.rotur.dev/mist/project": "https://git.rotur.dev/mist/project.git",
    "github.com/owner/tool": { "repository": "https://github.com/owner/tool.git", "ref": "v1.2.0" }
  },
  "goDependencies": {
    "github.com/google/uuid": "v1.6.0"
  },
  "scripts": {
    "dev": ["osl", "run", "main.osl"],
    "test": ["osl", "test"]
  },
  "bin": {
    "example": "main.osl"
  },
  "lint": {
    "rules": { "OSL-COMMENT": "warning" }
  }
}
FieldTypeMeaning
namestringRequired. The project's name. For a package other people install, use owner/repository.
versionstringThe project's version. opal init writes 1.0.0.
descriptionstringOne line about the project.
mainstringThe project's entry file. opal init writes main.osl.
oslstringThe OSL version the project is written for. Informational.
dependenciesobjectGit packages. Each key is the package's full name, such as git.rotur.dev/mist/project. The value is a repository URL, or an object with repository and an optional ref. opal add writes this field for you.
goDependenciesobjectGo modules, as "module/path": "version".
scriptsobjectNamed commands for opal run. Each is an array of arguments.
binobjectCommands the package provides, as "command": "file.osl".
lintobjectWarning levels for this project. See Lint settings.

Opal reads and rewrites the file with two-space indentation, and keeps lint exactly as you wrote it.

Scripts#

A script is an argument array, not a shell string. Opal runs the first item as the program and passes the rest as arguments, with no shell in between. Extra words after the script name are added to the end. The project's .opal/bin folder is put at the front of PATH, so scripts can call commands that packages provide.

{
  "name": "example",
  "scripts": {
    "dev": ["osl", "run", "main.osl"],
    "test": ["osl", "test"]
  }
}
Terminal
opal run              # list the scripts
opal run dev          # run one
opal run test src/    # extra arguments are appended: osl test src/

Scripts run with the project folder as their working directory. The exit status is Opal's: a failing script makes opal run exit with 1.

Packages and dependencies#

A package is a Git repository with an opal.json. Add one, and Opal clones it into .opal/packages, records the commit in opal.lock, and writes it to dependencies:

Terminal
opal add mist/project
opal add mist/[email protected]
opal add gh:owner/repository
opal add https://example.com/team/tool.git
opal add ../local-package

How the argument is read:

FormResolves to
owner/repogit.rotur.dev/owner/repo
host/owner/repoThat host, such as git.example.com/team/tool
rotur:owner/repogit.rotur.dev/owner/repo
gh:owner/repo or github:owner/repogithub.com/owner/repo
gitlab:owner/repogitlab.com/owner/repo
codeberg:owner/repocodeberg.org/owner/repo
A Git URL (https://..., git@host:owner/repo)That repository, named by host and path
A local folderThat folder, which must be a Git repository. It is named by its opal.json.
any of the above plus @refThe branch, tag or commit ref

Without a @ref, Opal follows the repository's default branch and locks the commit it finds.

Importing a package#

Import a package by its name. As with opal add, a name with two parts is shorthand for git.rotur.dev:

osl
import "mist/project"
import "github.com/owner/repository"

An import loads every .osl file in the package's top folder (apart from .test.osl files), the same way a folder import does. The modules lesson covers how imported names are used.

The lock file#

opal.lock records the commit of every Git package, including the packages those packages depend on, and the version and checksums of every Go module. Anyone who runs opal sync gets exactly those versions, whatever the packages have published since.

CommandEffect
opal syncRestore the project to what opal.lock says. Run it after cloning.
opal updateMove every direct Git dependency to the newest commit of its ref.
opal update <package>Move one dependency.
opal remove <package>Remove a dependency. A bare repository name works when it is unambiguous.

Add --offline to add, update or sync to forbid network access. Git packages must then already be in .opal/packages at the locked commit, and Go modules must already be in Go's module cache.

Inspecting dependencies#

Terminal
opal list
opal graph
opal why mist/project

list prints each locked dependency and its commit. graph prints the Git dependency tree. why prints the chain of packages that leads from the project to one dependency, and exits with 1 when there is no such chain. All three take --json.

Go dependencies#

Go packages from outside Go's standard library come in as Go modules. Add one with the go: prefix:

Terminal
opal add go:github.com/google/uuid
opal add go:github.com/google/[email protected]

Without a version after @, Opal uses latest and records the version it found. The module goes under goDependencies in opal.json, and its version and checksums go in opal.lock. Use it in a program with a go: import, as the Go lesson shows:

osl
import "go:github.com/google/uuid"

log uuid.NewString()

A package's own goDependencies join your project's lock. When both name the same module, your entry wins. To move a module to a newer version, add it again, for example opal add go:github.com/google/uuid@latest. Remove one with opal remove go:github.com/google/uuid. Builds fetch any module they are missing at the locked version.

Package commands#

A package can provide programs through the bin field, a map from command name to the .osl file that implements it:

{
  "name": "mist/tool",
  "version": "1.0.0",
  "bin": { "tool": "src/main.osl" }
}

Three ways to use one:

Terminal
opal exec mist/tool --help      # run once, from a temporary checkout
opal add mist/tool              # as a dependency: built into .opal/bin
opal add -g mist/tool           # install it for everyone on this machine

opal exec compiles the package's command, runs it with the arguments you give, and throws the build away. As a project dependency, the commands are built into .opal/bin, which opal run puts on PATH. With -g, they are built into $OPAL_HOME/bin, which is ~/.opal/bin unless you set OPAL_HOME. Put that folder on your PATH to call global commands by name.

Terminal
opal list -g
opal update -g [package]
opal remove -g mist/tool

Two packages cannot provide the same command name, and a command cannot replace a file that is already there. Pass --force to add, update or sync to replace it.

opal clean deletes the project's .opal folder. opal.lock stays, so opal sync brings everything back.

Lint settings#

The lint field turns the compiler's warnings up, down or off for one project. osl run, osl compile, osl test, osl lsp check and the editor all read it, and the editor rechecks open files when you save opal.json.

{
  "name": "example",
  "lint": {
    "rules": {
      "OSL-LOOSE-PARAM": "error",
      "OSL-COMMENT": "warning",
      "OSL-LONG-FUNCTION": ["warning", { "max": 80 }]
    },
    "overrides": [
      { "files": "src/legacy/", "rules": { "OSL-LONG-FUNCTION": "off" } }
    ]
  }
}

Each rule is named by its code (see the diagnostics reference for every code) and set to a severity, or to an array of a severity and options.

SeverityEffect
offNot reported.
pedanticReported only with --all-warnings. The editor shows it as a hint.
warningReported, and the program still compiles.
errorReported as an error. The warning's kind changes from ...Warning to ...Error, and compile, run, test and lsp check fail.

Only warnings can be configured. Compile errors such as TypeError always stay errors. An unknown code, a bad severity or a bad option is reported as a ConfigError at its place in opal.json.

overrides apply rules to the files that match files, a glob or a list of globs relative to the project folder. * matches within one folder, ** matches any depth, and a glob that names a folder (with or without a trailing /) matches everything inside it. Use / as the separator on every system. Overrides apply in order, and the last match wins. An override that sets only a severity keeps the options set before it. Packages installed in .opal/ always use the default severities.

A // osl-disable comment still silences a warning that the project has made an error.

Opt-in rules#

These four rules are off unless a project turns them on:

CodeReportsOptions
OSL-COMMENTComments, apart from osl-disable directives and the safety comment that raw() calls need. A run of comment lines counts once.
OSL-LONG-FUNCTIONFunctions longer than max lines.max, default 60
OSL-UNTYPED-GLOBALTop-level variables declared with auto, any or no type.
OSL-OBJECT-PARAMParameters declared as plain object or array, or collections of them, where a struct or typed collection would say what is expected.

Command summary#

Every command has a one-letter short form. Options: -g, --global manages installed commands, --offline forbids network access, --force replaces conflicting commands, and --json prints structured output.

CommandShortUsageEffect
initiopal init [name]Create opal.json in the current folder.
addaopal add <package> [--offline] [-g] [--force]Add and lock a Git or go: dependency.
removedopal remove <package> [-g]Remove a dependency.
updateuopal update [package] [--offline] [-g] [--force]Refresh locked commits.
syncsopal sync [--offline] [--force]Restore what opal.lock records.
listlopal list [-g] [--json]List locked dependencies.
graphgopal graph [--json]Print the dependency tree.
whywopal why <package> [--json]Show why a package is a dependency.
runropal run [script] [args...]List scripts, or run one.
execeopal exec <package> [args...]Run a package command once.
cleancopal cleanDelete .opal.
versionvopal versionPrint the version.
helphopal helpPrint the command summary.

Environment#

VariableEffect
OPAL_HOMEWhere global packages and commands live. Default ~/.opal.
OSL_CACHE_DIRThe compiler's cache folder, which Opal's builds use too.