Janet 1.3.1 Documentation
jpm
Most default installs of janet come with a build tool, jpm
, that makes
managing dependencies, building Janet code, and distributing Janet applications
easy. While jpm
is not required by Janet, it is a very handy build tool
that removes much of the boilerplate for creating new Janet projects, and is
updated in step with Janet. jpm
also works on Windows, linux, and macos
so you don't need to write multiple build scripts and worry about platform
quirks to make your scripts, binaries, and applications cross-platform.
jpm
's main functions are installing dependencies and building native janet
modules, but it is meant to be used for much of the life-cycle for janet
projects. Since Janet code doesn't usually need to
be compiled, you don't always need jpm
, especially for scripts, but jpm
comes with some functionality that is difficult to duplicate, like compiling janet source code
and all imported modules into a statically linked executable for distribution.
Glossary
A self contained unit of Janet source code as recognized by jpm
is
called a project. A project is a directory containing a project.janet
file, which contains build recipes. Often, a project will
correspond to a single git repository, and contain a single library. However, a
project's project.janet
file can contain build recipes for as many
libraries, native extensions, and executables as it wants. Each of these
recipes builds an artifact. Artifacts are the output files that will either be
distributed or installed on the end user or developer's machine.
Building Projects with jpm
Once you have the project to your machine, building the various artifacts should be pretty simple.
sudo jpm deps
jpm build
jpm test
jpm install
(On windows, sudo is not required. Use of sudo on posix systems depends if you installed janet to a directory owned by the root user.)
Dependencies
jpm deps
is a command that installs Janet libraries that this software depends on recursively.
It will automatically fetch, build, and install all required dependencies for you.
As of August 2019, this only works with git, which you need to have installed on your
machine to install dependencies. If you don't have git you are free to manually
obtain the requisite dependencies and install them manually with sudo jpm install
.
Building
Next, we use the jpm build
command to build artifacts. All built
artifacts will be created in the build
subdirectory of the current
project. Therefor, it is probably a good idea to exclude the build directory
from source control. For building executables and native modules, you will need
to have a c compiler on your PATH where you ran jpm build
. For posix
systems, the compiler is cc
.
If you make changes to the source code after building once, jpm
will
try to only rebuild what is needed on a rebuild. If this fails for any reason, you
can delete the entire build directory with jpm clean
to reset things.
Windows
For windows, the C compiler used by jpm
is cl.exe
, which is
part of MSVC. You can get it with Visual Studio, or standalone with the C and C++
Build Tools from Microsoft. You will then need to run jpm build
in
a Developer Command Prompt, or source vcvars64.bat
in your shell to add
cl.exe
to the PATH.
Testing
Once we have built our software, it is a good idea to test it to verify that it works
on the current machine. jpm test
will run all test scripts in the test
directory of the project and return a non-zero exit code if any fail.
Installing
Finally, once we have built our software and tested that it works, we can
install it on our system. For an executable, this means copying it to the
bin
directory, and for libraries it means copying them to the global
syspath. You can optionally install into any directory if you don't wan to
pollute your system or you don't have permission to write the directory
where janet itself was installed. You can specify the path to install modules
to via the --modpath
option, and the path to install binaries to with
the --binpath
option. These need to given before the subcommand
install
.
The project.janet
file
To create your own software in Janet, it is a good idea to understand what
the project.janet
file is and how it defines rules for building, testing, and
installing software. The code in
project.janet
is normal Janet source code that is run in a special environment.
Functions and macros from the cook
library have been imported, making then
easier and more natural to use. For example, cook/declare-project
can be referenced
as simply declare-project
.
A project.janet
file is loaded by jpm
and evaluated to create
various recipes, or rules. For example, declare-project
creates
several rules, including "install"
, "build"
, "clean"
, and
"test"
. These are a few of the rules that jpm
expects project.janet
to create when executed.
Declaring a Project
Use the declare-project
as the first declare-
macro
towards the beginning of your project.janet
file. You can also
pass in any metadata about your project that you want, and add
dependencies on other Janet projects here.
(declare-project
:name "mylib" # required
:description "a library that does things" # some example metadata.
# Optional urls to git repositories that contain required artifacts.
:dependencies ["https://github.com/janet-lang.json.git"])
Creating a Module
A 100% janet library is the easiest kind of software to distribute in Janet.
Since it does not need to be built, and installing it is simply moving
the files to a system directory, we only need to specify the files that
comprise the library in project.janet
(declare-source
# :source is an array or tuple that can contain
# source files and directories that will be installed.
# Often will just be a single file or single directory.
:source ["mylib.janet"])
For information on writing modules, see the modules docpage.
Creating a Native Module
Once you have written you C code that defines your native module
(see the embedding page on how to do this), you
must declare it in project.janet
in order for jpm
to build
the native modules for you.
(declare-native
:name "mynative"
:source ["mynative.c" "mysupport.c"]
:embedded ["extra-functions.janet"])
This make jpm
create a native module called mynative
when build
is run, and the arguments should be pretty straight forward. The
:embedded
argument is janet source code that will be embedded as an array
of bytes directly into the C source code. It is not recommended to use the
:embedded
argument, as one can simply create multiple artifacts, one for
a pure C native module and one for Janet source code.
Creating an Executable
The declaration for an executable file is pretty simple.
(declare-executable
:name "myexec"
:entry "main.janet")
jpm
is smart enough to figure out from the one entry file what libraries
and other code your executable depends on, and bundles them into the final application
for you. The final executable will be located at build/myexec
, or build\myexec.exe
on windows.
Also note that the entry of an executable file should look different than a
normal janet script. It should define a main
function that can receive
a variable number of parameters, the command-line arguments. It will be called
as the entry to your executable.
(import mylib1)
(import mylib2)
# This will be printed when you run `jpm build`
(print "build time!")
(defn main [& args]
# You can also get command-line arguments through (dyn :args)
(print "args: " ;(interpolate ", " args))
(mylib1/do-thing)
(mylib2/do-thing))
It's important to remember that code at the top level will run when you invoke
jpm build
, not at executable runtime. This is because in order to create
the executable, we marshal the main
function of the app and write it
to an image. In order to create the main function, we need to actually compile
and run everything that it references, in the above case mylib1
and
mylib2
.
This has a number of benefits, but the largest is that we only include bytecode
for the functions that our application uses. If we only use one function from
a library of 1000 functions, our final executable will not include the bytecode
for the other 999 functions (unless our one function references some of those
other functions, of course). This feature, called tree-shaking, only works
for Janet code. Native modules will be linked to the final executable statically
in full if they are used at all. A native module is considered "used" if is imported
at any time during jpm build
. This may change, but it is currently the most reliable
way to check if a native modules needs to be linked into the final executable.
There are some limitations to this approach. Any dynamically required modules will not
always be linked into the final executable. If require
or import
is not called
during jpm build
, then the code will not be linked into the executable. The module
can still be required if it is available at runtime, though.
For an example Janet executable built with jpm
, see https://github.com/bakpakin/littleserver.