Essential reading:
A self-contained unit of Janet source code as recognized by
jpmis called a project. A project is a directory containing aproject.janetfile, which contains build recipes. Often, a project will correspond to a single git repository, and contain a single library. However, a project'sproject.janetfile 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.
project.janet format is informal and is just a slightly modified janet script - bakpakin
Absolute minimum requirement is (declare-project :name "blah").
declare-project can have further k:v pairs:
:name string (jpm will use this e.g. for jpm uninstall) (this is not what (import ...) uses):description a string:dependencies an array of strings, urls to git repos with required artifacts:url then :repo:url:repo:version(declare-source :source ... )
:source file-name(s) (this is what (import ...) uses!):prefix - will create a directory in to put the source files in (and import will use this as its target)(declare-native ...)
:name:source array of file names as strings:embedded array of file names as strings:cflags - example: https://github.com/sogaiu/janet-pcg-random/blob/6c531fd9060a9fa26467cd4a3552dd88f02a8d63/project.janet:headers(declare-executable ...)
:name:entry filename string:install boolean(declare-bin ...)
:main path executable's installed to(declare-binscript ... ) declares a janet file installed as executable script
hardcode-syspath bool - if true, inserts code so it'll run even if JANET_PATH changes:is-janet bool - if true, inserts a shebang line if jpm has :auto-shebang set true(declare-headers...)
:headers - path:prefix(declare-manpage ...)Spork has a pretty complicated project.janet file.
Here is an example which builds the legacy project.janet from the modern bundle's info.jdn.
In project.janet, I think and (declare-project :name ...) are the minimal requirements, and ... everything else is not significant to the tooling (e.g. license or version)?
Interesting reading:
Pyrmont copypastes a hybrid project.janet which uses info.jdn as its source of truth:
(def info (-> (slurp "info.jdn" ) parse ))
(declare-project
:name (info :name )
:description (info :description )
:author (info :author )
:license (info :license )
:url (info :url )
:repo (info :repo )
:dependencies (info :dependencies ))
(task "bundle-install" ["manifest" ]
(def lman-path (find-manifest (info :name )))
(def lman-data (when (= :file (os/stat lman-path :mode ))
(parse (slurp lman-path ))))
(assert lman-data "no legacy manifest file" )
(if (bundle/installed? (info :name ))
(bundle/replace (info :name ) "." )
(bundle/install "." ))
(def bdir-path (-> (string/slice (find-manifest-dir ) 0 -11 )
(string "bundle/" (info :name ))))
(def bman-path (string bdir-path "/manifest.jdn" ))
(def bman-data (when (= :file (os/stat bman-path :mode ))
(parse (slurp bman-path ))))
(assert bman-data "no bundle manifest file" )
(each p (reverse (bman-data :files ))
(array/push (lman-data :paths ) p ))
(array/push (lman-data :paths ) bdir-path )
(spit lman-path (string/format "%j\n" lman-data ))
(array/push (bman-data :files ) lman-path )
(spit bman-path (string/format "%j\n" bman-data )))
(add-input "install" "bundle-install" )
He does this to:
Use the installation logic in the bundle script rather than relying on JPM’s installation logic (an ordinary project file typically uses this logic by calling declare-source, declare-bin, etc).
Copy the relevant information from the modern manifest into the legacy manifest so that you can uninstall using JPM and it will ensure everything is removed properly.
The way it achieves the first purpose is by making the install task (a built-in task that JPM provides) dependent on the install-bundle task. This is what happens on line 33 (setting the input of a task A to be another task B, ensures that JPM calls task B before task A).
The install-bundle task is defined in lines 12-31. First, it makes the manifest task an input to the install-bundle task. The manifest task creates a manifest file (what I call the ‘legacy manifest’ because it is distinct from the manifest created for modern bundles). Second, it calls bundle/install (or bundle/replace if the bundle is already installed). Third, it works out where the directory that will save the info file, the bundle script and the modern manifest will be (/bundle/). Fourth, it reads the modern manifest at this location and then the paths in the modern manifest into the legacy manifest.
jpm is currently used by the vast majority of projects.
Clojure's leinigen inspired jpm but Bakpakin et al. find jpm's declarative pproach wanting, so it's being replaced with janet-pm and modern bundles resembling Clojure's tools.build where "your build is a program":
The philosophy behind
tools.buildis that your project build is inherently a program - a series of instructions to create one or more project artifacts from your project source files. We want to write this program with our favorite programming language, Clojure, andtools.buildis a library of functions commonly needed for builds that can be connected together in flexible ways. Writing a build program does take a bit more code than other declarative approaches, but can be easily extended or customized far into the future, creating a build that grows with your project.
info.jdn or bundle/info.jdn
:name is required (but there's a backdoor to call bundle/install on a bundle without an info.jdn file.)bundle/init.janet or bundle.janet which Pyrmont calls "bundle script"janet-pm seems to use :jpm-dependencies and :dependencies at points
Example:
The bundle/ module is a package manager without networking, letting Package managers built on top, handle user workflows, building whole projects by determining and downloading dependencies etc.
Summarizing, it works globally or on a project.janet file for backwards compatability, whose extra complexity some dislike. janet-pm is in spork/pm which some don't like (the new-simple-project command creates a sporkless project.)
janet-pm development kind of stalled, because it has some new quirks and tries to do things more like jpm did and less like the new bundle stuff does. In that way, it tries to support so-called old-style packages with a
project.janetfile.
The help text explains a lot.
Jeep builds on top of modern bundles (instead of? or on top of janet-pm?) For Jeep, in info.jdn these fields are significant:
:url and :repo provide URLs for jeep-new:dependencies https://github.com/pyrmont/jeep/blob/53b484eb7a071faa4d2c9646940096aa75a8ddc4/lib/install.janet#L53There are two approaches to package managers.
The first approach is to have things be highly declarative and rely on your package manager to take that data and effectively convert it to code that runs. At its best, this makes things feel 'magical' because you just declare some stuff and the package manager magically makes something happen.
The second approach is to put as much of the code into the package. At its best, this makes things very robust because, at the extreme end, you don't need the package manager at all—everything is in the package (together with the language runtime).
What I like about what @bakpakin has done is that by cleanly separating the pieces into a declarative piece (the info file) and a functional piece (the bundle script), there's the potential to have your cake and eat it too (in a manner of speaking).
If have a tool (e.g. Jeep) that copies a bundle script into your project, you don't need to write the functional part by hand but it's there in the bundle and so can be used even without the bundle manager present.
Why make Jeep?
I can’t speak very intelligently about janet-pm as I’ve never actually used it. And JPM doesn’t do anything with the info file or the bundle script. That just leaves Jeep.
So I’ve tried as much as possible to avoid having Jeep rely on information in the info file. From memory, the three places where it does are: (1) installation of transitive dependencies with jeep prep system; (2) installation of vendored dependencies with jeep prep vendor; and (3) generation of an API document with jeep api.
Otherwise, I put the logic into the bundle script. Then all Jeep does really is call bundle/install and then bundle/install just executes the bundle hooks. These are (1) postdeps, (2) build and (3) install (there is also clean and check but these are optional and Jeep doesn’t tell bundle/install to execute them).
Each bundle hook effectively calls a function with the same name in the bundle script. So if you have a bundle script with a build function, then it’ll be called during the execution of bundle/install. It doesn’t actually need to build anything. The return value isn’t used by bundle/install. It’s just called and then, when it returns, bundle/install moves to the next step in its execution.
Another reason was that I think if you have multiple tools, it helps encourage standards (think web browsers) and I would like for people in the future to be able to make their own package managers. So creating an alternative to janet-pm makes it less likely for de facto standards that are never defined anywhere because all that matters is that it works in janet-pm.
Why try avoid having Jeep rely on the info file?
(1) I think this is consistent with the way that bundle/install has been designed.
(2) By pushing logic into the bundle script rather than putting it in Jeep, this means that you don't need Jeep. You can use a different bundle manager and everything should still work because the logic is local to your bundle, not my tool.
jeepvery much usesinfo.jdn. i am trying to make some of my existing repositories work withjeepand have been placing information withininfo.jdnspecifically forjeepto process.the information on these lines is specifically stuff that
jeepprocesses. no other program (e.g.janet-pm,jpm, etc.) knows what to do with that stuff. - sogaiu
What are bundle hooks
So I don't know if you're trying to view this on your phone but the bundle/install function is here (https://github.com/janet-lang/janet/blob/master/src/boot/boot.janet#L4331-L4410).
In terms of what the bundle hooks are, they're keywords (e.g. :install) which are converted in the do-hook function to symbols (e.g. install) which is then used to pull a function with that name from an environment table that is generated by the bundle script.
Put more simply, they're a 'safe' way to call functions with these names. If they don't exist in the environment table, nothing happens.
how do
project.janetandinfo.jdndiffer?
Proposed at these periods:
clojure maintainers made their own thing and one of the key files was deps.edn.
in clojure, there was a project.clj file which leiningen used (and still uses). janet's project.janet is somewhat like project.clj. here is a sample project.clj file (the devs made this up for demo purposes iiuc).
here is an example deps.edn file and a project.clj file from the same project.
project.janet can have calls to functions, right? info.jdn doesn't.
deps.edn and info.jdn are data files that are meant to contain data only -- no functions for example:
declare-* things are functions. this is a function call.project.janet as well as import stuff, etc. here is an example that imports -- it also uses one of the imported things (path/join). for a more sophisticated example, see this -- it has function definitions as well.afaiu, info.jdn by constrast is meant to contain a single table or struct (at least atm).
further, the content of info.jdn is only parsed (not evaluated!) by the bundle/* bits in boot.janet. that can be observed here. so iiuc one is not meant to put function calls within info.jdn.
terminology. also I like "namespace"
You might want to look at require. That's the lower level function that import uses to actually get the imported file's bindings into an environment table. You might already be at this point so apologies if I'm simply telling you things you know but a critical piece of understanding for me was to realise that Janet 'namespaces' are really just environment tables and that environment tables are really just tables with the value in each key-value pair itself being a table with a particular structure.
Janet prefers the terms module and package over namespace and library. The source code for use, import, require etc. in boot.janet uses module a lot.
Outside of the core, bundles are directories in the file system, containing info.jdn and a bundle script. Bundles can provide one or more modules by specifying files to copy into Janet's lib directoryvia a bundle script's install function. A module's a logical grouping of bindings, which an environment table represents in the runtime, constructed by reading a file with dofile (which import uses.) (root-env holds Janet's core binding: (= root-env (getproto (curenv))))
A module is a collection of bindings and its associated environment. By default, modules correspond one-to-one with source files in Janet, although you may override this and structure modules however you like. - official docs
In clojure, you would not see "multilevel" names like clojure.string/fun/join but only clojure.string/join with a namespace (clojure.string) with a name (join). Janet does not have "namespaces" (similarities are skin-deep). In Janet, / doesn't carry the same implications of a module or single file (things can e.g. be defined in various c files, even) and we see things like spork/math/factorial, but there is no namespace; the entire thing is the function name.
Generally, modules are a single file, but relative imports and :prefix "" :export true fake a single module without many prefixes. (import joy) makes its symbols (bindings) available, prefixed with joy/. You can modify or remove the "prefix".
Eternal thanks to Sogaiu and Pyrmont!