There has never been anything like CPAN for LambdaMOO. While there have always been dumps of raw/nearly raw moocode on the web, installing any particular collection is about as painless as a trip to the oral surgeon. The deep inheritance hierarchy, the single core architecture, the lack of organization at any level beyond an object, etc. all get in the way. Forget about sharing code with others, it's not even easy for me to share code with myself!

The Decade of Easy

I'm impatient—I want to use a library of useful objects by typing something like:

@install foobar

NodeJS has only been around since 2009 (two years, at the time of this writing), but it has a package manager (npm) that makes it easy to install third-party code. (NodeJS also comes with a fancy introductory video, which was popularized by David Heinemeier Hansson for Rails, for the truly impatient—watch it on your iPhone while you ride the bus to work...!) The same is true for Ruby (Gems) and Python... I'm sure there are exceptions, but I trust you get the point. In the last ten years, we've come to expect that software—even open-source software—is going to be super-easy to install and use.

This article is ostensibly about packaging LambdaCore. However, it's really an introduction to the package management functionality built into the Stunt kernel, which is a foundational part of Stunt. So, before I start talking about hoisting LambdaCore, I need to describe the Stunt architecture.

The Stunt Architecture

LambdaMOO's design encourages a style of development that is agile (it's no surprise to me that agile/extreme programming came out of the Smalltalk community), iterative, and immersive—which is incredible, IMHO—but it's sometimes like monkey patching is the order of the day, everyday. As a result, in most LambdaMOO applications, everything except the server itself resides in a single layer in the database.

For the sake of modularity, Stunt imposes some structure. The application (whatever it is—game, social space, web site...) sits on top of a suite of building blocks—reusable library code in the form of packages. The cores themselves (Stunt Core, LambdaCore, JHCore...) are really northing more than large packages. All of the library packages sit on top of the Stunt kernel, which is itself a package—albeit one with a special role. Everything runs on top of the server.

The Stunt Kernel

The Stunt kernel is meant to be a core-neutral layer that:

  • fixes issues with the server that haven't made it to C code
    (for example, the respond_to() bug in the latest server)
  • provides common functionality that most/all MOO cores can share
    (security, serialization/deserialization, packaging)

Relevant to packaging, the most important objects in the kernel are Shapes, the serialization/deserialization library, and Composed, the package manager.

Shapes (object #8 in an out-of-the-box Stunt database) knows how to serialize/deserialize an object, preserving type and structural information about the object in the process. Composed (object #9) uses Shapes to import/export/install/uninstall collections of objects, reproducing their relationships and interconnections with full fidelity.

With the Stunt kernel, it's trivial to install packages of objects—instructions are found all over this site (@install foobar with $composed). The package manager knows how to find a package in an archive, check prerequisites, retrieve the representation of that package, and install that package in a server—in the process, recreating the objects, properties, and verbs that make up the package, and properly relocating the object numbers so relationships stay intact.

Package Basics

A package is just a collection of objects. The members of a package are all those objects which are inside (listed in the contents property of) the package, and they are imported/exported as a group, with all important relationships intact. A package minimally has an identifier property, which must be globally unique, and a version property. A package may also define manifest, relocate, requires, provides, description, etc. properties. There is no canonical parent for all package objects. It's enough to have the necessary properties.

It's intended that applications sit on top of the Stunt kernel and take advantage of its package management functionality, to better manage—and share—code. In order to test the provided functionality, I turned LambdaCore into a package. Here's how I did it.

Hoisting LambdaCore

LambdaCore is a reasonably large body of code (97 tightly-coupled objects). It's larger than most packages that I expect to see built in the near-term, so it makes an excellent test case. My goal is to turn the LambdaCore database into a package that can be installed into another compatible database with a single command.

Creating a New Package

LambdaCore runs perfectly on the Stunt version of the LambdaMOO server. I used the version of the LambdaCore database from May 17, 2004, which I took to be the latest that is publicly available.

A package is literally a container for objects with a set of well-known properties defined on it. By convention, packages own themselves. The new package will have no parent—it could, but this constraint keeps things simple. Let's assume the new object will be assigned the object number #100. As a wizard, I created a new object (I spent so long hacking on Minimal.db that I use eval for pretty much everything):

; create($nothing, $nothing)
; #100.name = "LambdaCore Package"

Minimally, a package should have an identifier and a version.

; add_property(#100, "identifier", "lambdacore", {#100.owner, "r"})
; add_property(#100, "version", "0.0.0", {#100.owner, "r"})

The entire contents of the original LambdaCore database go inside the package. I only moved valid objects that are nowhere (their location equals $nothing)—in other words, I preserved the location/contents relationships between existing objects in LambdaCore:

; for o in [#0..#99]; if (valid(o) && o.location == $nothing); move(o, #100); endif; endfor

At this point, if I wanted to, I could call it quits and proclaim victory—I have a perfectly well-formed package. However, the package, if exported, would still have objects with object numbers from the old database—not so useful when it's installed in its new home. In order to fix this, I need to specify relocation information. But before I do that, I need to add Shapes and Composed to the server running LambdaCore, since I will need them to export the package.

Injecting Shapes and Composed

Shapes and Composed are part of the kernel package, which isn't part of LambdaCore. However, they are both self-contained, single-object libraries that inject well into existing cores via a variety of means. For LambdaCore derived cores it's sufficient to paste a @dump-style dump of each object right into the console—assuming the console is running a simple network client like telnet; or I can use a tool like netcat, which is the sweetness. Suitable dumps are available as Github gists:

The two dumps assume they are for objects with numbers #101 (Shapes) and #102 (Composed). If you try this yourself, you will need to edit the dumps and change the object numbers if this is not the case.

The Manifest

A package can have an optional manifest property. The manifest assigns labels to objects in the package. The labels are used in the package file representation and will be used later when specifying the relocations that must occur when importing/exporting the LambdaCore package (you will also learn that you can use the labels to look up objects dynamically). There are a lot of objects in LambdaCore, so there's a lot of manifest! I've trimmed it down to a few objects for brevity:

; add_property(#100, "manifest", {{#100, "package"}, {#0, "dictionary"}, {#1, "root_class"}, {#2, "wizard"}, {#3, "room"}, {#7, "exit"}, {#50, "generic_editor"}, {#40, "mail_recipient_class"}}, {#100.owner, "r"})

The Relocation Information

Composed knows it has to preserve certain relationships between objects when exporting/importing a package. If object #1 (Root Class) is the parent of object #3 (generic room) in the original core, then object #1274 (Root Class) must be the parent of object #1281 (generic room) in RandomCore (for example) when the lambdacore package is installed in RandomCore. This is true for the properties owner and location/contents, as well as the attributes parents/children.

But what do I do about a property like home on #2 (Wizard), which references object #62 (The First Room) in the original database? The optional relocate property holds references to properties on objects in the package that will need to have their object number values relocated when the package is installed. The format of this property is a list of "<label>.<property>" pairs, where the label is the label used in the manifest. Once again, the following has been trimmed for brevity.

; add_property(#100, "relocate", {"wizard.home", "wizard.features", "wizard.owned_objects", "wizard.current_folder", "generic_editor.help", "mail_recipient_class.help"}, {#100.owner, "r"})

If a property holds an object number but that property is not listed in the relocate property, when the package is installed in another database the property will continue to hold the original object number—this probably isn't what you want.

Cleanup

The steps above got me about 80% of the way to the complete solution. There are a handful of properties and configuration steps that can't be quite so cleanly encapsulated in relocation information—updating the player database object, for example; and inserting the system object (object #0 in the original core) as a parent of object #0 in the new database (so that dollar-sign abbreviations like $player_db work).

The optional property instructions specifies common post-processing steps that packages might want Composed to take as part of the installation process. The property holds a list—the value "install-dictionary" instructs Composed to look for a object with the label "dictionary" in the manifest of the package being installed, and to make it a parent of object #0.

; add_property(#100, "instructions", {"install-dictionary"}, {#100.owner, "r"})

When a package is installed, Composed will call the verb after_install() after it completes all of its automated processing, if a suitable verb is defined on the package object. The following verb definition makes a handful of important changes to the objects in the newly installed package. It uses a few features of the Stunt kernel I have not introduced yet. The verb $lookup() dynamically looks up the object number of a label using the package's manifest property. It's a simple alternative to the global namespace for properties/values that are only used internally within a package. The verb $permit() is part of the built-in security infrastructure in the Stunt kernel. It's a shortcut that ensures that the calling verb has wiz-perms.

; add_verb(#100, {#100.owner, "xd", "after_install"}, {"this", "none", "this"})
@program #100:after_install
args && raise(E_ARGS);
$permit("wizard");
/* update $sysobj.class_registry */
generics = {"root_class", "room", "exit", "thing", "note", "letter", "container", "player", "prog", "wiz", "generic_editor", "mail_recipient", "mail_agent"};
for i in [1..length(generics)]
  generics[i] = $lookup(generics[i]);
endfor
utilities = {"string_utils", "list_utils", "wiz_utils", "set_utils", "gender_utils", "math_utils", "time_utils", "match_utils", "object_utils", "lock_utils", "command_utils", "perm_utils", "building_utils", "seq_utils", "byte_quota_utils", "object_quota_utils", "code_utils", "matrix_utils", "convert_utils", "biglist"};
for i in [1..length(utilities)]
  utilities[i] = $lookup(utilities[i]);
endfor
$sysobj.class_registry[1][3] = generics;
$sysobj.class_registry[2][3] = utilities;
/* update $player_db */
$player_db.frozen = 1;
$player_db:clearall();
players = {"wizard", "hacker", "housekeeper", "editor_owner", "no_one"};
for i in [1..length(players)]
  players[i] = $lookup(players[i]);
endfor
for p in (players)
  $player_db:insert(p.name, p);
  for a in (p.aliases)
    $player_db:insert(a, p);
  endfor
endfor
$player_db.frozen = 0;
.

I also need to remove properties defined on the system object in the LambdaCore database that are already defined correctly in the Stunt kernel, or that have no function in a package.

; delete_property(#0, "server_options")
; delete_property(#0, "nothing")
; delete_property(#0, "ambiguous_match")
; delete_property(#0, "failed_match")

And we're done with the definition! All that remains is to export the JSON representation of the package to a file. This JSON representation is the format in which packages are shared.

Exporting

Before I export, I want to mention auditing. The Composed object defines a verb named audit() that, given a package, reports on what's wrong with it. It will warn you when there is a property on an object that is holding an object number and that property is not listed in relocate; likewise, it will warn you if there are naked object numbers in any of the verb code in the package. These are just warnings—you can ignore them if you know better. It will also point out errors that will prevent a successful export—when the manifest property references an object that simply does not exist, for example. Before you export, always audit! It will save you many headaches! (Now remember, #100 is the package, #101 is Shapes, and #102 is Composed.)

; #102:audit(#100)

Now I need to 1) install the package locally, and then 2) export the JSON representation of the package to a file.

; #102:install(#100)
; #102:export_package_to_file("lambdacore", "0.0.0", "/lambdacore_0_0_0.json")

Once complete, the "files" directory now contains the file "lambdacore_0_0_0.json".

The Package Properties

These are all of the required and optional properties that may be specified on package objects.

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.