A dependency management system for Lua on the Playdate.
Lua Rocks, unfortunately, isn't compatible with the Playdate implementation
of Lua. Playdate uses import to pull in files and libraries at compile
time, whereas Lua Rocks (and mainline Lua) uses require that pulls in
things at runtime. The distinction matters and probably has a lot to do
with running on embedded hardware, but suffice to say that while Playdate
supports nearly all of Lua, it does not support the package ecosystem.
Git submodules are a fine way to manage repositories that you just want to
clone down and update. Using toybox gives you that ability as well as the
ability to manage transitive dependencies (and their proper, non-conflicting
versions) in those repositories.
You can always grab a binary from the releases page, but there are also a few other options for installation.
If you have homebrew, then you can use it to manage your toybox install:
brew tap jm/toybox
brew install toybox
On first run, macOS will likely flag it as a security risk. You'll need to go into System Preferences / Security & Privacy to allow it. I'm working on fixing that!
Grab the .msi package from the releases page. I'm working on getting a package
ready for winget.
Clone the code, run go get, and then go build.
A toybox dependency can either be specifically tailored to use with toybox,
or a simple Git repository that follows most recommended Playdate development
patterns. I've tried to make it accommodate what I see most folks doing and
Panic recommending, so it should require little to no work to setup a library
to work with toybox.
To start, you'll need a file named Boxfile to your root folder. A Boxfile
is a simple JSON file that is a single object document. The object's keys
are repo identifiers (<GitHub username>/<repository name>) and the values
are the version requirements. You can add a dependency using the add command
(and remove them using the remove command) like so:
toybox add Nikaoto/deep
Now your Boxfile should look like this:
{
"Nikaoto/deep": "default"
}
The default version requirement will grab whatever the default branch for that
repository is (usually master or main). You can also specify that using *
or the actual branch name. You can optionally just edit your Boxfile like a
normal text file to add the configuration, but the commands make it easy!
For varying version requirements (for example, "anything newer than 1.0" or
"greater than 2.0 but less than 3.0"), you can specify those in the same way.
Let's pretend we wanted any version newer than 1.0 of the deep library:
{
"Nikaoto/deep": "> 1.0"
}
Versions in toybox are specified by Git tags on the repository, so if a library
owner wanted to publish a version, they simply have to tag it with a
semver version number like 1.0 or 2.4.1 or 1.3.6beta
or some such. The toybox client will parse those and then check the version
constraints specified in the various Boxfiles it resolves when installing to find
the best version.
Once you have a Boxfile setup, you simply run:
toybox install
This command will resolve and install the needed dependencies specified in your
Boxfile (and the Boxfiles of your dependencies, and their dependencies, and
so on). The packages are downloaded to source/libraries and namespaced by
toybox name. Then a single import file is generated at source/toyboxes.lua:
import("libraries/Nikaoto/deep/deep")
So to import all of your toyboxes to your game, simply add:
import "toyboxes"
...and they should be available to use.
Toybox will also handle getting the dependencies of your dependencies. So let's
pretend that the Nikaoto/deep library depended on another library named jm/geometry.
In the Boxfile in the Nikaoto/deep repository, it would look something like this:
{
"jm/geometry": "default"
}
Now if you ran toybox install, the output would look something like this:
🧸toybox v.0.1
Loading Boxfile...
Resolving dependencies...
Installing
Fetching jm/geometry@main
Fetching Nikaoto/deep@master
Writing import file
And the import file might look like (in toyboxes.lua):
import("libraries/jm/geometry/main")
import("libraries/Nikaoto/deep/deep")
Noting that it imports the dependency library before the deep library
(though that doesn't matter currently due to the way the Playdate SDK
compiles things, it's still a good future-proofing just in case!).
Toybox will resolve these dependencies in an infinitely deep graph (i.e.,
it will get dependency of dependencies of dependencies of dependencies...),
so you only need to focus on your immediate dependencies and let toybox
take care of the rest.
The toybox binary has a few other subcommands.
If you're doing a lot of dependency changes, you might run up against
the GitHub API's rate limiting on unauthenticated requests. If this
happens, you'll start seeing the request status for things like getting
a list of versions returning a 403 status rather than 200. To fix
that, you'll need to provide a GitHub personal access token.
Following the instructions in the above link, you can generate a token that you'll need to copy and paste into the prompts when asked:
toybox login
Toybox will ask for your username and the token. Once provided, the token will be passed with each request. Authenticated API limits are much higher and shouldn't cause you any issues going forward.
Note: When generating the token, it's best if you choose a sensible
expiration (60 days or so?) and only add the repo:public_repo scope.
That way if your token somehow gets compromised, the only thing the person
who has it can do it read public repositories (which is all toybox needs
to do unless you have private dependencies in private repos).
To generate a new Playdate project pre-wired for toybox and with a few
extra goodies (project structure, pdxinfo, .gitignore, Makefile,
etc.), use the generate command:
toybox generate path/to/your/project
This command will drop a new Playdate project in the given path (named
for the last part of the path), that you can immediately run make and
run in the Simulator.
You can add and remove dependencies manually if you'd like, but there are also convenience commands for doing that:
toybox add owner/dependency
toybox remove owner/dependency
These commands will add or remove a dependency from your Boxfile and
resolve/install the list again.
If you want to update a single dependency's version (either because you changed it manually or because there is a newer one available), you can run:
toybox update owner/dependency
Running this command will find the best version to resolve with the provided version constraints (including the changed version for the dependency you're updating).
To find out the current toybox version, simply run toybox version.
To see the current list of dependencies, run toybox info.
To get more help, run toybox help followed by a subcommand. For
example, toybox help install.
Creating a library compatible with toybox is actually really simple
since I tried to make toybox understand conventions and norms that
the PlayDate dev community is already using.
Ideally, your code will be contained in a source folder with a
import.lua file that is the entrypoint for your library.
Realistically, though, your code simply needs to have one of the
following files to be properly imported by toybox:
/source/import.lua/source/main.lua/source/<your toybox name>.lua(e.g.,jm/Geometry/source/Geometry.lua)/import.lua/main.lua/<your toybox name>.lua(e.g.,jm/Geometry/Geometry.lua)
That's about it. Once someone imports your package using toybox,
the code will be available after they import the toyboxes.lua file.
If your toybox depends on other toyboxes, you can add a Boxfile
to your library and toybox will resolve and import those into
your users' downstream code as well. So, for example, if jm/Geometry
depends on you/Things and a user added the Geometry library to their
project's Boxfile, the dependency list shown by toybox info would
list both of those dependencies as being in their toybox bundle.
Folks can always install your toybox from master or main or whatever
the default branch is for your repository. They can install that version,
and when new commits are added, they can run toybox update to get the
newest code. Ideally, though, your library would be versioned so users
can better manage its impact on their projecrts.
To make a versioned release, you need to tag a commit in your Git repository and push that tag to GitHub. You can do this from the command line with these instructions or you can do it in the GitHub UI with theirfancy Releases feature.
The tag's name needs to follow Semantic Versioning. So for example, good tag names would be something like:
v8.0v0.2.00.1.1beta
You can name your releases whatever you'd like, but the version resolution might get interesting if you don't follow semantic versioning. 😬
File issues for any bugs or feature requests!
If you want to contribute code, please create a feature branch and submit a pull request.
