As we’ve already seen, it’s easy to install a directory of Racket modules as a package with raco pkg, Racket’s package-management utility. So if our jsonic directory looks like this:
1 2 3 4 5 | jsonic ├╴ tokenizer.rkt ├╴ reader.rkt ├╴ parser.rkt ··· |
From the command line, we can install it like so:
1 2 | > cd path/to/jsonic > raco pkg install |
Or, equivalently:
1 | > raco pkg install path/to/jsonic |
One little gotcha—this variant won’t work:
1 2 | > cd path/to > raco pkg install jsonic |
In this case, because jsonic doesn’t look like a directory name, raco pkg tries to find the jsonic package using Racket’s remote package server. (Which won’t work, because it doesn’t know about jsonic. But we’ll fix that problem shortly.) In this situation, we can induce raco pkg to use the local directory like this:
1 2 | > cd path/to > raco pkg install ./jsonic |
Recall that once we install a directory as a package, we can import its modules into another Racket program with (require jsonic/module-name). Or, if the package contains a language, use it in a source file as #lang jsonic. For prototyping purposes, this suffices.
But if we want to control the installation more precisely—e.g., automatically build our new documentation—we need to add an "info.rkt" file.
The "info.rkt" file contains metadata that tells Racket how to handle the contents of the directory. When Racket installs a package, it looks for an "info.rkt" in the top directory, and each subdirectory it encounters. If it doesn’t find one, installation proceeds by default.
In general, once we’re ready to share our language, we’ll want to add an "info.rkt" to the top directory of the package that refines how the package is compiled, installed, and tested.
A Racket source file is part of several organizational schemes at once. In practice, these can overlap. So let’s make sure the terminology is clear before we move forward: + Hat tip to Vincent St-Amour for these explanations.
A module is the basic organizational unit of code. Every source file contains a single module at the top level. (As we know from past tutorials, the #lang line expands into a single module expression.) This is why source files in Racket are also called modules.
A package is the basic unit of code distribution. A package is a named group of modules that are installed together. In our simple example above, we put our modules into a directory called jsonic, and used raco pkg to install this as a package, also called jsonic.
A collection is a namespace of related modules. For instance, when we say (require math/statistics), we’re actually asking for the statistics module from the math collection.
Though packages contain the actual code modules, a collection is not necessarily “owned” by any single package. Rather, any package can make modules available under any collection (or even multiple collections). So even if math/statistics lives in the math package, another package foo-pkg can provide math/alpha, and package bar-pkg can provide math/omega.
Collection names are inferred from directory structure when not otherwise specified. In our simple example above, when we installed the package jsonic, raco implicitly used jsonic as the collection name too. (But, as we’ll see below, we can override this assumption with "info.rkt".)
A repo or repository is the canonical location where the source files are stored (e.g., on GitHub). A package offered to the public via Racket’s package server is associated with a repo or path within a repo, from which the package is installed. So when we issue a command like this:
1 | > raco pkg install beautiful-racket |
Racket consults the package server to find the repo for the beautiful-racket package, and downloads the source from there.
Racket uses repos strictly to download source files. It otherwise imposes no requirements on the organization of repos. In many cases, a Racket package has its own repo. But one repo can also be home to multiple packages.
What can sometimes make these terms confusing is that one name can be reused in several roles. For instance, consider our jsonic project:
1 2 3 4 5 | jsonic ├╴ tokenizer.rkt ├╴ reader.rkt ├╴ parser.rkt ··· |
In this case, our package is called jsonic, and lives in a repo called jsonic. When we install this with raco pkg, our modules become part of the jsonic collection. Easy.
A more complex Racket project might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 | mega-repo ├╴ info.rkt ├╴ foo-pkg ├╴ info.rkt └╴ math ├╴ alpha.rkt └╴ another.rkt └╴ bar-pkg ├╴ info.rkt ├╴ math └╴ omega.rkt └╴ games └╴ red.rkt |
Here, the mega-repo is home to two independent packages, foo-pkg and bar-pkg. The developer would list these separately on the package server, so users could install them separately. foo-pkg provides modules that are part of the math collection, while bar-pkg provides modules that are part of the math and games collections. Meanwhile, the "info.rkt" files in each directory would make these intentions clear.
BTW, this is not a project organization we’d necessarily want to emulate. + This capability is typically not used to smush unrelated modules into common packages, but rather to divide larger packages into smaller ones. It simply illustrates that repos, packages, and collections are independent ways of grouping modules. In simple projects, they’re often telescoped together.
The "info.rkt" file uses a domain-specific language called #lang info, which looks like regular Racket but only supports a limited set of functions. The only purpose of #lang info is to make "info.rkt" files. So let’s start our file at the top level of the jsonic directory:
1 | #lang info |
An "info.rkt" is structured as a series of define expressions that act as metadata fields. Various Racket tools read these metadata fields. If we want our package to cooperate with these tools, we need at least one "info.rkt" in the top directory of the package. We can also put "info.rkt" files inside any subdirectory that needs special handling. But often, the top-level file suffices.
Since we’re planning to distribute our language, we want to start by specifying a collection name and a version number:
1 2 3 | #lang info (define collection "jsonic") (define version "1.0") |
Per the explanation above, the collection is the namespace where our package is available. In this case, we want the collection value to be the same as the package name—"jsonic". Strictly speaking, when the collection and package names match, this collection field is optional. But it’s a virtuous habit to set it explicitly. That way, we remove the connection between the two names. If we ever rename the package, or move these modules to be part of another package, they will still correctly be available as part of the jsonic collection.
Strictly speaking, the version field is also optional—without it, raco pkg treats jsonic as having version 0.0. But when we’re developing a language, it’s another virtuous habit. Version numbers help those who rely on our language understand the relationship between successive releases. Also, if other developers write packages that depend on our language, the version number helps raco pkg figure out if a user needs a more recent version of our language.
The "info.rkt" also specifies how to build the documentation for our language. To do this, we define the scribblings field with a list of .scrbl documentation files. In this case, we have only one:
1 2 3 4 | #lang info
(define collection "jsonic")
(define version "1.0")
(define scribblings '(("scribblings/jsonic.scrbl")))
|
The raco test tool is used to run tests. When used with the -p flag, it runs all tests that exist in a package. Meaning, for each source file in the package, raco test runs the test submodule (if it exists), or the whole file. + raco test is commonly used in shell scripts for services that automatically build and test Racket packages, for instance Travis CI.
We can see how this works by entering the following command at the terminal:
1 | > raco test -p jsonic |
We’ll get something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | raco test: "/path/to/jsonic/buttons.rkt" raco test: (submod "/path/to/jsonic/colorer.rkt" test) raco test: "/path/to/jsonic/expander.rkt" raco test: (submod "/path/to/jsonic/indenter.rkt" test) raco test: "/path/to/jsonic/info.rkt" raco test: "/path/to/jsonic/jsonic-test.rkt" [ null, 42, true, ["array","of","strings"], {"key-1":null,"key-3":{"subkey":21},"key-2":false} ]raco test: "/path/to/jsonic/main.rkt" raco test: "/path/to/jsonic/parser-test.rkt" raco test: "/path/to/jsonic/parser.rkt" raco test: "/path/to/jsonic/reader.rkt" raco test: "/path/to/jsonic/scribblings/jsonic.scrbl" raco test: (submod "/path/to/jsonic/tokenizer.rkt" test) 13 tests passed |
"jsonic-test.rkt" prints its JSON result to the terminal. Nothing bad happens as a result.
But let’s suppose we prefer our test reports to be tidy. So we want to omit "jsonic-test.rkt" from our test suite.
We can do this by adding a test-omit-paths field to our "info.rkt", which is a list of source files that raco test should ignore:
1 2 3 4 5 | #lang info
(define collection "jsonic")
(define version "1.0")
(define scribblings '(("scribblings/jsonic.scrbl")))
(define test-omit-paths '("jsonic-test.rkt"))
|
After we make this change, we can run our tests again:
1 | > raco test -p jsonic |
This time, "jsonic-test.rkt" is skipped:
1 2 3 4 5 6 7 8 9 10 11 12 | raco test: "/path/to/jsonic/buttons.rkt" raco test: (submod "/path/to/jsonic/colorer.rkt" test) raco test: "/path/to/jsonic/expander.rkt" raco test: (submod "/path/to/jsonic/indenter.rkt" test) raco test: "/path/to/jsonic/info.rkt" raco test: "/path/to/jsonic/main.rkt" raco test: "/path/to/jsonic/parser-test.rkt" raco test: "/path/to/jsonic/parser.rkt" raco test: "/path/to/jsonic/reader.rkt" raco test: "/path/to/jsonic/scribblings/jsonic.scrbl" raco test: (submod "/path/to/jsonic/tokenizer.rkt" test) 13 tests passed |
Exactly right.
A dependency is any Racket package that’s necessary for our language to work. We use "info.rkt" to list both run-time dependencies (= packages needed every time we invoke a module from the package) and build dependencies (= packages only needed for testing or documentation). + Build dependencies are specified separately so they can be omitted from built packages, allowing them to be smaller.
When a user tries to install a package, raco pkg checks the user’s existing packages against the dependencies for the new package. If any are missing, raco pkg gives the user the option to install them.
But we probably forgot to keep track of all the dependencies that we racked up during our project. Fortunately, that’s not a problem: we can use the raco setup command to tell us what they are. Let’s go to the terminal and issue this command:
1 | > raco setup --check-pkg-deps jsonic |
raco setup is responsible for compiling every installed package. When we pass the --check-pkg-deps flag, raco setup inspects all our modules and determines their dependencies, and compares these to the dependencies declared in our "info.rkt" (which right now is none). As it does so, we see a bunch of undeclared dependencies go flying up the screen. But at the end we get a summary like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | raco setup: --- summary of package problems --- raco setup: undeclared dependency detected raco setup: for package: "jsonic" raco setup: on packages: raco setup: "base" raco setup: "beautiful-racket-lib" raco setup: "brag" raco setup: "draw-lib" raco setup: "gui-lib" raco setup: "br-parser-tools-lib" raco setup: "rackunit-lib" raco setup: "syntax-color-lib" raco setup: on package for build: raco setup: "scribble-lib" |
This tells us all the packages that need to be listed in "info.rkt" as ordinary dependencies (the packages in the top part of the list) and build dependencies (just scribble-lib).
We can add these dependencies to our "info.rkt" manually. But more conveniently, raco setup can take care of this housekeeping. If we’re satisfied with the list of dependencies, we can issue another terminal command to put them into our "info.rkt":
1 | > raco setup --fix-pkg-deps jsonic |
If we now reopen our "info.rkt", we see it’s been updated with the list of deps and build-deps that we just saw in the missing-dependency report:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #lang info (define collection "jsonic") (define version "1.0") (define scribblings '(("scribblings/jsonic.scrbl"))) (define test-omit-paths '("jsonic-test.rkt")) (define deps '("base" "beautiful-racket-lib" "brag" "draw-lib" "gui-lib" "br-parser-tools-lib" "rackunit-lib" "syntax-color-lib")) (define build-deps '("scribble-lib")) |
How do we know this worked? We can go back to the terminal and use --check-pkg-deps again:
1 | > raco setup --check-pkg-deps jsonic |
This time, it shows no errors, because all the dependencies are now correctly declared in our "info.rkt". If we ever change jsonic to use different dependencies, we can either manually update these lists, or use raco setup with the --fix-pkg-deps flag to adjust them.
The dependency list we just made contained only unversioned dependencies. When we specify a dependency without a version number, what it means is “any version of this package is fine”. That works, as long as we’re only relying on parts of the package interface that have been consistent across every version.
But suppose that brag is updated to version 2.0 with a new xyzzy function that’s essential to our project. We can no longer specify brag as a unversioned dependency, because our language won’t work for users who only have version 1.0 of brag installed.
We can fix this problem by adding a version annotation to brag:
1 2 3 4 5 | (define deps '("base" "beautiful-racket-lib" ("brag" #:version "2.0") "draw-lib" ···)) |
In this case, when raco pkg installs our language, it checks if the user has brag version 2.0 or later installed. If brag isn’t installed at all, raco pkg automatically installs the current version (as it would an unversioned dependency). But if a version of brag earlier than 2.0 is installed, raco pkg updates brag to the current version.
But in this case, we can leave our brag dependency unversioned.
Our finished "info.rkt" looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #lang info (define collection "jsonic") (define version "1.0") (define scribblings '(("scribblings/jsonic.scrbl"))) (define test-omit-paths '("jsonic-test.rkt")) (define deps '("base" "beautiful-racket-lib" "brag" "draw-lib" "gui-lib" "br-parser-tools-lib" "rackunit-lib" "syntax-color-lib")) (define build-deps '("scribble-lib")) |
To make sure that it works, let’s uninstall our package like so:
1 | > raco pkg remove jsonic |
Then reinstall as usual:
1 | > raco pkg install path/to/jsonic |
We should see some status messages as the package is installed. But no errors.
Let’s try running our tests again:
1 | > raco test -p jsonic |
We should see no errors.
Now let’s test the documentation with the raco docs command, which is just command-line shorthand for using the search box in the web interface:
1 | > raco docs jsonic |
Our web browser should show us a results screen like this:
That’s also correct—in the first search result, the documentation system is correctly noticing the anchor to the jsonic language. For the second result, it’s noticing the word "jsonic" in the headline. If we click either of these links, we’ll see our main documentation page:
And now, if we go into DrRacket and run a test file:
1 2 3 4 5 6 7 8 9 10 11 | #lang jsonic // a line comment [ @$ 'null $@, @$ (* 6 7) $@, @$ (= 2 (+ 1 1)) $@, @$ (list "array" "of" "strings") $@, @$ (hash 'key-1 'null 'key-2 (even? 3) 'key-3 (hash 'subkey 21)) $@ ] |
It works as usual:
1 2 3 4 5 6 7 | [ null, 42, true, ["array","of","strings"], {"key-1":null,"key-3":{"subkey":21},"key-2":false} ] |
Great. Now that our jsonic package has been prepared with an "info.rkt" file, we’re ready to share it with the world using the package server.