What is a Package?

A package is just a nested collection of objects and a few conventions about how those objects relate and interact. Packages use the location/contents properties of objects to define the package and what is packaged. The challenge when packaging and import/exporting MOOcode is fixing up the object references between the object in the package, and the objects in the rest of the application. Composed handles this chore (and others) for you.

This document describes the package format and the process of packaging through a series of progressively more complicated examples. If you just want to cowboy-up then drop down to the section titled The Recipe and have at it. Otherwise, read on. Hands-on examples are in the sections The Simplest Package and The Next Simplest Package.

Features

The Stunt | Improvise package manager is called Composed (AKA $composed). It provides the following features:

  • automatic relocation of packaged objects
  • package dependencies and automatic prerequisite enforcement
  • before/after package install hooks
  • centralized hosting of shared packages
  • neutral JSON-based package format

Lifecycle

Roughly speaking, a package is created, exported, imported, installed and uninstalled. Currently (this will change) package building and management is meant to be performed by someone with wiz-perms.

Your Tools

The following function and verb calls are the ones commonly used to work with packages.

Create

The create() built-in function is used to create a new package. At its simplest, a package is just an object that contains other objects. There’s no required parent class, verbs or properties.

Export/Import

$composed maintains a local cache of packages. This cache is useful when building a package (as well as for backups). The verbs $composed:export_package_to_cache() and $composed:import_package_from_cache() use $composed:export() and $composed:import(), which serialize and deserialize entire collections of MOO objects – but you shouldn’t have to use those verbs directly. If you want to export a package outside of Stunt to share with others, use $composed:export_package_to_file().

Install/Uninstall

The verbs $composed:install() and $composed:uninstall() add/remove a nested collection of packaged objects to/from $composed.

The Simplest Package

The simplest package is a single object with no references to any other objects:

; create($nothing, $nothing) /* => #12345 */
; #12345.name = "Example Package"

It’s helpful if a package has an identifier and a version. By convention (my convention, starting now), the package identifier “example” is reserved for documentation.

; add_property(#12345, "identifier", "example", {#12345, "r"})
; add_property(#12345, "version", "0.0.0", {#12345, "r"})

“Example Package” is now a complete package. The next step is to tell $composed about it (to install it as a known package in the package manager).

; $composed:install(#12345)

The package now appears in the list of installed packages:

@list packages with $composed
Updating...
Installed packages
...
 ! example, 0.0.0 (#12345) Example Package
...

You can still modify a package after it’s been installed. In fact, a common workflow is to create the package, install it locally, and then incrementally modify it, export it, import it, test it and repeat.

; add_property(#12345, "test", {}, {#12345, "r"})
; add_verb(#12345, {#12345, "rxd", "test"}, {"this", "none", "this"})

The package cache is a local (in-MOO) cache of serialized packages. To export the package to the cache:

; $composed:export_package_to_cache("example", "0.0.0")

If everything works, this verb will store a serialized representation of the package to the cache, and dump the serialized representation to the console for inspection. Listing the packages will now show the package in the cache.

@list packages with $composed
Updating...
Installed packages
...
   example, 0.0.0 (#12345) Example Package
...
Cached packages [local]
...
   example, 0.0.0
...

Test the new package by uninstalling the original package (this will not delete the original package), and then importing the exported package from the cache.

; $composed:uninstall("example", "0.0.0")
@install example with $composed
Updating...
Installing version "0.0.0" of package "example" from the local cache...
Version "0.0.0" of package "example" (Example Package) was successfully installed as #12346.

If the export/import fails for some reason or the packaged functionality is not correct, delete the imported package and try again. Note: be very careful with $composed:delete() because it deletes the package and all of its contents!

; $composed:delete(#12346)
; $composed:install(#12345) /* the original was not deleted, only uninstalled */

To export the package to a file (in JSON format) for safekeeping or to share:

; $composed:export_package_to_file("example", "0.0.0", "example_0.0.0.json")

A list of existing packages is available at Stunt.io. Contact Todd Sundsted if you want to include packages of your own construction in the repository.

The Next Simplest Package

A slightly less simple package is a nested collection of objects with no references to objects outside of the package. Begin by taking the following steps to bump up the version number of the example package.

; $composed:uninstall("example", "0.0.0")
; #12345.version = "0.0.1"
; $composed:install(#12345)

Create a few more objects and move those objects inside the package. Note: by convention the owner of these objects should be the package (this avoids an external object reference to the player, or any other object).

; create($nothing, #12345) /* => #12347 */
; #12347.name = "Another Object"
; move(#12347, #12345)

“Another Object” (and any other objects you added to the package) now reference the object number of the package (specifically, the owner and location properties of the new objects hold the value #12345). $composed already knows that owner, location and an object’s parents are object numbers and will attempt to relocate them automatically. Furthermore, since they are all members of the same package, no other information is necessary to do this successfully. The following steps export the new version of the package to the cache and then import it again to test the packaging. The new package has the same object layout and relationships as the original.

; $composed:export_package_to_cache("example", "0.0.1")
; $composed:uninstall("example", "0.0.1")
@install example with $composed

Once verified, revert back to the original (in preparation for the next example).

; $composed:delete("example", "0.0.1")
; $composed:install(#12345)

The Manifest

A package manifest gives meaningful, predictable names to the objects in a package. A manifest is optional in the simple examples above, but is required both for specifying properties on objects in the package that hold object numbers and will need relocating, and for relocating object references across packages. A package manifest is just a property on the outermost (or top) object in the package. (Don’t forget to bump up the version of the example package to version “0.0.2”).

; add_property(#12345, "manifest", {{#12345, "package"}, {#12347, "object"}}, {#12345, "r"})

A manifest is a set of pairs – object name and label. Export the example package and look at the serialized representation. Notice that the labels in the manifest are used as keys for the serialized objects. Import the package and look at the package manifest. Notice that the labels are associated with the correct object numbers in the imported package (not the original package).

By convention some labels identify objects with special roles in a package. See the section Special Roles below for a description of the existing roles.

Properties That Need Relocation

$composed will relocate the owner and location properties, and an object’s parents. It won’t inspect and relocate other properties unless it’s told that those properties hold object numbers and require relocation. The list of properties to relocate is specified in the relocate property on the package object. (Don’t forget to bump up the version of the example package.)

; add_property(#12345, "relocate", {}, {#12345, "r"})

In the manifest, object #12347 (“Another Object”) was labeled “object”. Add a property to this object, and put the object number of the package object in this property (the object number of any object in the package will work – the next section will describe how to specify dependencies on other packages).

; add_property(#12347, "foobar", #12345, {#12345, "r"})

The relocate property is a list of properties on objects in the package that hold object numbers that will need to be relocated when the package is exported and imported. The format is <label>.<property> where label is the label given to the object in the manifest.

; #12345.relocate = {"object.foobar"}

When the package is exported and subsequently imported, the object number in the foobar property will point to the correct object. (Try it!)

Dependencies Between Packages

The objects in a package may depend on the objects in another package to function correctly. The verbs may access properties or call verbs on the objects in the other package. The objects may inherit from the objects in the other package. The requires property on the package object lists these dependencies. (Bump the version.)

; add_property(#12345, "requires", {}, {#12345, "r"})

Assume the contained object #12347 (“Another Object”) in the package inherits from #4 (“Object”) in the kernel package.

; chparents(#12347, {#4})

In this scenario, $composed must be told how to deal with the external object reference, both on export and on import. The requires property holds a simple set of rules that dictate the specific package and version of every dependency.

; #12345.requires = {{"kernel", "1.0"}}

The value above says that this package depends on any version of the kernel package as long as the major version number is “1” and the minor version number is “0”. The third value, the release version number, can be any number. This package will now export and import correctly. More importantly, if the example package were imported on a MOO where “Object” has a different object number, that would be taken care of on import.

Special Roles

The manifest property assigns descriptive labels to the objects in a package. Some objects in a package assume special roles based on the labels they are given.

Dictionary

The most significant role is that of dictionary. A dictionary is an object that defines properties that hold the object numbers of other objects in the package (of course, it can define other kinds of properties and verbs, as well). Dictionaries are meant to be associated with the system object (#0), either via a property on the system object or as a parent of the system object. In that case, the properties and verbs on the dictionary are accessible via the common dollar sign ($) prefix notation. See the section on Post-Processing for guidance on how this is accomplished in the context of a package.

; create($nothing, #12345) /* => #12348 */
; #12348.name = "Dictionary"
; move(#12348, #12345)
; #12345.manifest = {@#12345.manifest, {#12348, "dictionary"}}

Changelog

The object in the manifest that is identified as the changelog holds properties whose names are timestamps and whose values describe changes made to that package on of before that time. By convention, timestamps are generated with tostr(time()).

Package

By convention, the package object itself is identified as package.

Post-Processing

Complex packages may require post-installation steps in order to wire the package into the database correctly. This includes everything from fixing up properties that are not simple object numbers to customizing verbs on the package (I’m not advocating the latter, but existing bodies of code do exactly that).

Instructions

The most common post-processing step involves wiring an installed package to the system object (#0). Properties and verbs on the system object, its parents, and indirectly on objects referred to in properties are accessible via dollar-sign ($) prefix notation, which is a customary way to ensure location independence in existing core databases.

The instructions property defines a list of post-processing steps to take on installation (and to reverse on uninstallation). These steps all wire the dictionary to the system object one way or another.

If the package defines a dictionary (see the section on Special Roles) the instruction “install-namespace” will add a property to the system object with the same name as the package and the object number of the dictionary in that package. This is the preferred method because it avoids cluttering up the top-level namespace with hundreds/thousands of values.

The following adds the package dictionary as a property on the system object. If the dictionary defines the properties “foo” and “bar”, they will be accessible as $example.foo and $example.bar after the package is installed.

; add_property(#12345, "instructions", {"install-namespace"}, {#12345, "r"})

The instruction “install-dictionary” makes the package dictionary a parent of the system object. This is less desirable because it clutters the global namespace, but is necessary for some legacy packages.

Before/After Hooks

The package manager handles many common kinds of post-installation work automatically, however it can’t anticipate everything. The before/after hooks mechanism is a general purpose, programmatic way to make adjustments to a package after it is installed.

Composed will currently attempt to call four hooks during package installation and uninstallation: before_install, after_install, before_uninstall and after_uninstall. Of the four, after_install and before_uninstall are the most useful.

The hooks are simply verbs defined on the package object. They must be executable (the x flag must be set) and have sufficient permissions to perform the necessary operations.

; add_verb(#12345, {#12345.owner, "xd", "after_install"}, {"this", "none", "this"})

It’s good practice to restrict the callers allowed to call the hooks. The expression $restrict_to_caller($composed) prevents verb calls from any callers other than the $composed package manager.

@program #12345:after_install
$restrict_to_caller($composed);
/* operations to perform after the package is installed */
/* ... */
.

Property Reference

This is a list of all of the required and optional properties that may be specified on a package object.

  • identifier :: required Identifies a package. Must be globally unique.
  • version :: required The version of the package, in major.minor.patch format.
  • manifest :: optional A mapping of object to labels.
  • relocate :: optional Relocation information for the package, in “label.property” format.
  • requires :: optional The packages that this package requires. The property is a list of items of the form {“identifier”, “version specifier”}; the version specifier has the form “<|>|= version” (e.g. “> 1.0”, “= 1.2.2”).
  • provides :: optional A package provides itself as a prerequisite by default. This is a list of items of the form {“identifier”, “version”}. It specifies additional prerequisites that this package provides—useful when a package is an aggregate of other packages.
  • instructions :: optional Post-processing steps that Composed should take once a package is imported and relocated.
  • description :: optional The description of this package.
  • authors :: optional A list of the authors of this package.
  • homepage :: optional The URI of the homepage of this package.
  • license :: optional The name of the license for this package.

Problems

Object References

The most common problem by far is stray object references (object numbers) inside of packaged objects. Object “FooBar” with object number #10000 on MOO-A won’t have the same object number on MOO-B. This is absolutely true of the objects in the package itself. Object numbers are found in the parents attribute and the location and owner properties, as well as in property values and verb code. $composed goes to some trouble to convert object numbers to symbolic names on export, and to convert those names back to appropriate object numbers (a process known as “relocation”) on import. A correctly configured package makes that easy/possible. If $composed can’t figure out what to do with an object number or symbolic name for some reason, it will raise an error that tries to pinpoint the problem. The solution usually involves getting rid of the object reference, adding appropriate relocation information to the package, or adding a pre/post-procesing step to fix the value.

The Recipe

The basic recipe for creating a new package follows:

  • Create the package object. Add identifier, version, manifest, relocate and requires properties, and any of the optional package properties.
  • Give the package an identifier and a version.
  • Move the objects that will be part of the package inside the new package object. In every case, make note of the properties that hold object numbers and the packages where those references objects reside.
  • Add all of the prerequisite packages to requires. As a first pass, it’s often sufficient to just list the corresponding package identifiers ({{"kernel"}, {"foo"}, {"bar"}}) – this effectively wildcards the version.
  • Add an entry in manifest for every object in the package.
  • Add an entry in relocate for every property to relocate.
  • Test the package with $composed:export(<package>). This verb returns the serialized form of the package for inspection. If there are problems it will raise errors. Scan the serialized form for the string “error” – the serialization code will add these entries when it encounters problems with permissions, visibility, etc.
  • Fix problems and repeat until clean.
  • Export to cache or file.

Hoisting LambdaCore

Read Hoisting LambdaCore for a description of the steps involved in creating a really big package.