Packages
System.put_env("AL_MNESIA_DISTRIBUTED", "false")
System.put_env(
"AL_MNESIA_DIR",
Path.join(System.tmp_dir!(), "al_livebook_#{:erlang.unique_integer([:positive])}")
)
Mix.install(
[{:al, github: "anoma/AL-Ex"}],
config: [
al: [
transaction_programs: [
AL.TransactionProgram.Bootstrap,
AL.TransactionProgram.PackageSystem
],
package_channels: [{:builtin, {:priv, "packages"}}],
package_environment: [:interval],
natives: []
]
]
)
use AL
A package groups AL definitions so they can be discovered, installed, extended, exported, and imported together. Packages and their active builds are durable AL objects, and each branch has its own active package environment.
Configuration and channels
config :al,
transaction_programs: [
AL.TransactionProgram.Bootstrap,
AL.TransactionProgram.PackageSystem
],
package_channels: [
{:builtin, {:priv, "packages"}},
{:local, "/absolute/path/to/packages"}
],
package_environment: [:interval, :constraints]
package_channels are ordered places to find package bundles. package_environment lists the packages to activate at startup.
{
AL.Package.configured_channels(),
AL.Package.configured_environment(),
AL.Package.active?(:interval)
}
{:ok, available} = AL.Package.available()
Enum.map(available, fn package ->
Map.take(package, [:package, :version, :channel])
end)
When a requested package declares dependencies, AL chooses providers whose requirements can all be satisfied and activates the resulting graph automatically. An atom dependency means “any provider for this package.” A {package, requirement} dependency asks the package object's accepts_requirement/4 method whether a provider is suitable.
Earlier channels are preferred, but resolution can use a later provider when necessary to satisfy the whole graph. After changing package configuration on a running node, call AL.Package.update_configured/1 to rediscover and activate it.
Package bundles
A filesystem package contains a manifest and its definitions:
greetings/
├── package.al
└── definitions/
├── greeting.class.al
└── list.extension.al
Package {
#name : :greetings,
#version : 1,
#deps : []
}
A .class.al file defines a class owned by the package. An .extension.al file adds methods or superclasses to a class owned elsewhere.
The package object model
The source files are portable representations of a small durable object model:
| Object | Meaning |
|---|---|
| channel | An ordered source of package bundles |
| package provider | A version of a package offered by one channel |
| package | The durable identity and policy object for a package name |
| package build | One exact provider plus its selected dependency builds |
channel -> provider -> build <- active package
The package name is itself a class. For example, :interval is a :package, while its exact build is an instance of :interval. Its :active_build slot selects the definitions currently in use.
run do
class(:interval, :package)
active_build(:interval, build)
class(build, :interval)
build_provider(build, provider)
provider_channel(provider, channel)
end
Builds record the classes they own and the methods or inheritance edges they add to existing classes. This lets packages be exported, replaced, and removed without confusing their contributions with somebody else's.
Create a package in AL
Creating a package gives it an open active build. Define normal AL classes and methods, then attribute them to that build.
author = AL.Branch.head()
{:atomic, _} =
run do
new(
:package,
%{name: :greetings, version: 1, deps: [], redef: true},
:greetings
)
active_build(:greetings, build)
defclass :greeting,
super: :object,
redef: true,
ivars: [words: []] do
defmethod(:words, [self, words]) do
get(self, :words, words)
end
end
include_class(build, :greeting)
defmethod(:list, :as_greeting, [words, greeting]) do
new(:greeting, %{words: words}, greeting)
end
include_method(build, :list, :as_greeting)
end
The package owns :greeting and extends the existing :list class.
run do
as_greeting([:hello, :package], greeting)
words(greeting, words)
active_build(:greetings, build)
originates_class(build, :greeting)
adds_method(build, :list, :as_greeting)
end
Use include_superclass(build, class, superclass) when the package adds an inheritance edge.
Export a package
Export writes the active build's attributed definitions as a portable bundle.
export_directory =
Path.join(
System.tmp_dir!(),
"al_greetings_package_#{System.unique_integer([:positive])}"
)
{:ok, exported} = AL.Package.export(:greetings, to: export_directory)
%{
package: exported.package,
manifest: File.read!(Path.join(export_directory, "package.al")),
definitions:
export_directory
|> Path.join("definitions/*.al")
|> Path.wildcard()
|> Enum.map(&Path.basename/1)
}
The exported source is now the build's reference, so AL.Package.diff(:greetings) shows later live changes.
Import a package
Import registers a bundle, builds it, and activates its definitions on the selected branch. This example forks from transaction zero so the branch begins without :greetings.
consumer = AL.Branch.fork(0, author)
AL.Branch.checkout(consumer)
:ok =
AL.TransactionProgram.install_all([
AL.TransactionProgram.Bootstrap,
AL.TransactionProgram.PackageSystem
])
AL.Package.active?(:greetings, consumer)
{:ok, imported} = AL.Package.import(export_directory, branch: consumer)
Map.take(imported, [:package, :build, :provider, :definitions])
run branch: consumer.id do
as_greeting([:hello, :consumer], greeting)
words(greeting, words)
end
Package activation is branch-local. Importing or changing packages on consumer does not change author; forking a branch from its current tip would instead copy its current package environment.
Inspect a package
%{
active_build: AL.Package.active_build(:greetings, consumer),
builds: AL.Package.builds(:greetings, consumer),
providers: AL.Package.providers(:greetings, consumer),
source: AL.Package.source_snapshot(:greetings, branch: consumer),
diff: AL.Package.diff(:greetings, branch: consumer)
}
Cleanup
AL.Branch.checkout(author)
AL.Branch.discard(consumer)
File.rm_rf!(export_directory)