Powered by AppSignal & Oban Pro

Packages

livebooks/packages.livemd

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)