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.
The Stunt | Improvise package manager is called Composed (AKA $composed
). It provides the following features:
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.
The following function and verb calls are the ones commonly used to work with packages.
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.
$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()
.
The verbs $composed:install()
and $composed:uninstall()
add/remove a nested collection of packaged objects to/from $composed
.
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.
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)
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.
$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!)
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.
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.
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"}}
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())
.
By convention, the package object itself is identified as package.
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).
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.
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 */ /* ... */ .
This is a list of all of the required and optional properties that may be specified on a package object.
major.minor.patch
format.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 basic recipe for creating a new package follows:
identifier
, version
, manifest
, relocate
and requires
properties, and any of the optional package properties.identifier
and a version
.requires
. As a first pass, it’s often sufficient to just list the corresponding package identifiers ({{"kernel"}, {"foo"}, {"bar"}}
) – this effectively wildcards the version.manifest
for every object in the package.relocate
for every property to relocate.$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.Read Hoisting LambdaCore for a description of the steps involved in creating a really big package.