6. Package Management¶
This document describes futhark pkg
, a minimalistic package
manager inspired by vgo. A
Futhark package is a downloadable collection of .fut
files and
little more. There is a (not necessarily comprehensive) list of
known packages.
6.1. Basic Concepts¶
A package is uniquely identified with a package path, which is
similar to a URL, except without a protocol. At the moment, package
paths must be something that can be passed to git clone
. In
particular, this includes paths to repositories on major code hosting
sites such as GitLab and GitHub. In the future, this will become more
flexible. As an example, a package path may be
github.com/athas/fut-foo
.
Packages are versioned with semantic version numbers of the form X.Y.Z
. Whenever versions are
indicated, all three digits must always be given (that is, 1.0
is
not a valid shorthand for 1.0.0
).
Most futhark pkg
operations involve reading and writing a package
manifest, which is always stored in a file called futhark.pkg
.
The futhark.pkg
file is human-editable, but is in day-to-day use
mainly modified by futhark pkg
automatically.
6.2. Using Packages¶
Required packages can be added by using futhark pkg add
, for example:
$ futhark pkg add github.com/athas/fut-foo 0.1.0
This will create a new file futhark.pkg
with the following contents:
require {
github.com/athas/fut-foo 0.1.0 #d285563c25c5152b1ae80fc64de64ff2775fa733
}
This lists one required package, with its package path, minimum version (see Version Selection), and the expected commit hash. The latter is used for verification, to ensure that the contents of a package version cannot be changed silently.
futhark pkg
will perform network requests to determine whether a
package of the given name and with the given version exists and fail
otherwise (but it will not check whether the package is otherwise
well-formed). The version number can be elided, in which case
futhark pkg
will use the newest available version. If the package
is already present in futhark.pkg
, it will simply have its version
requirement changed to the one specified in the command. Any
dependencies of the package will not be added to futhark.pkg
,
but will still be downloaded by futhark pkg sync
(see below).
Adding a package with futhark pkg add
modifies futhark.pkg
,
but does not download the package files. This is done with
futhark pkg sync
(without further options). The contents of each
required dependency and any transitive dependencies will be stored in
a subdirectory of lib/
corresponding to their package path. As an
example:
$ futhark pkg sync
$ tree lib
lib
└── github.com
└── athas
└── fut-foo
└── foo.fut
3 directories, 1 file
Warning: futhark pkg sync
will remove any unrecognized files or
local modifications to files in lib/
(except of course the package
directory of the package path listed in futhark.pkg
; see
Creating Packages).
Packages can be removed from futhark.pkg
with:
$ futhark pkg remove pkgpath
You will need to run futhark pkg sync
to actually remove the files in
lib/
.
The intended usage is that futhark.pkg
is added to version
control, but lib/
is not, as the contents of lib/
can always
be reproduced from futhark.pkg
. However, adding lib/
works
just fine as well.
6.2.1. Importing Files from Dependencies¶
futhark pkg sync
will populate the lib/
directory, but does
not interact with the compiler in any way. The downloaded files can
be imported using the usual import
mechanism (Referencing Other Files);
for example, assuming the package contains a file foo.fut
:
import "lib/github.com/athas/fut-foo/foo"
Ultimately, everything boils down to ordinary file system semantics. This has the downside of relatively long and clumsy import paths, but the upside of predictability.
6.2.2. Upgrading Dependencies¶
The futhark pkg upgrade
command will update every version
requirement in futhark.pkg
to be the most recent available
version. You still need to run futhark pkg sync
to actually
retrieve the new versions. Be careful - while upgrades are safe if
semantic versioning is followed correctly, this is not yet properly
machine-checked, so human mistakes may occur.
As an example:
$ cat futhark.pkg
require {
github.com/athas/fut-foo 0.1.0 #d285563c25c5152b1ae80fc64de64ff2775fa733
}
$ futhark pkg upgrade
Upgraded github.com/athas/fut-foo 0.1.0 => 0.2.1.
$ cat futhark.pkg
require {
github.com/athas/fut-foo 0.2.1 #3ddc9fc93c1d8ce560a3961e55547e5c78bd0f3e
}
$ futhark pkg sync
$ tree lib
lib
└── github.com
└── athas
├── fut-bar
│ └── bar.fut
└── fut-foo
└── foo.fut
4 directories, 2 files
Note that fut-foo 0.2.1
depends on github.com/athas/fut-bar
,
so it was fetched by futhark pkg sync
.
futhark pkg upgrade
will never upgrade across a major version
number. Due to the principle of Semantic Import Versioning, a new major version is a
completely different package from the point of view of the package
manager. Thus, to upgrade to a new major version, you will need to
use futhark pkg add
to add the new version and futhark pkg
remove
to remove the old version. Or you can keep it around - it is
perfectly acceptable to depend on multiple major versions of the same
package, because they are really different packages.
6.3. Creating Packages¶
A package is a directory tree (which at the moment must correspond to a Git repository). It must contain two things:
A file
futhark.pkg
at the root defining the package path and any required packages.A package directory
lib/pkg-path
, wherepkg-path
is the full package path.
The contents of the package directory is what will be made available
to users of the package. The repository may contain other things
(tests, data files, examples, docs, other programs, etc), but these
are ignored by futhark pkg
. This structure can be created
automatically by running for example:
$ futhark pkg init github.com/sturluson/edda
Note again, no https://
. The result is this futhark.pkg
:
package github.com/sturluson/edda
require {
}
And this file hierarchy:
$ tree lib
lib
└── github.com
└── sturluson
└── edda
3 directories, 0 files
Note that futhark pkg init
is not necessary simply to use
packages, only when creating packages.
When creating a package, the .fut
files we are writing will be
located inside the lib/
directory. If the package has its own
dependencies, whose files we would like to access, we can use
relative imports. For example, assume we are creating a package
github.com/sturluson/edda
and we are writing a Futhark file
located at lib/github.com/sturluson/edda/saga.fut
. Further, we
have a dependency on the package github.com/athas/foo-fut
, which
is stored in the directory lib/github.com/athas/foo-fut
. We can
import a file lib/github.com/athas/foo-fut/foo.fut
from
lib/github.com/sturluson/edda/saga.fut
with:
import "../foo-fut/foo"
6.3.1. Releasing a Package¶
Currently, a package corresponds exactly to a GitHub repository
mirroring the package path. A release is done by tagging an
appropriate commit with git tag vX.Y.Z
and then pushing the tag to
GitHub with git push --tags
. In the future, this will be
generalised to other code hosting sites and version control systems
(and possibly self-hosted tarballs). Remember to take semantic
versioning into account - unless you bump the major version number (or
the major version is 0), the new version must be fully compatible
with the old.
When releasing a new package, consider getting it added to the central package list. See this page for details.
6.3.2. Incrementing the Major Version Number¶
While backwards-incompatible modifications to a package are sometimes
unavoidable, it is wise to avoid them as much as possible, as they
significantly inconvenience users. To discourage breaking
compatibility, futhark pkg
tries to ensure that the package
developer feels this inconvenience as well. In many cases, an
incompatible change can be avoided simply by adding new files to the
package rather than incompatibly changing the existing ones.
In the general case, the package path also encodes the major version
of the package, separated with a @
. For example, version 5.2.1 of
a package might have the package path github.com/user/repo@5
. For
major versions 0 and 1, this can be elided. This means that multiple
(major) versions of a package are completely distinct from the point
of view of the package manager - this principle is called Semantic
Import Versioning, and is
intended to facilitate backwards compatibility of packages when new
versions are released.
If you really must increment the major version, then you will need to
change the package path in futhark.pkg
to contain the new major
version preceded by @
. For example,
lib/github.com/sturluson/edda
becomes
lib/github.com/sturluson/edda@2
. As a special case, this is not
necessary when moving from major version 0 to 1. Since the package
path has changed, you will also need to rename the package directory
in lib/
. This is painful and awkward, but it is less painful and
awkward than what users feel when their dependencies break
compatibility.
6.3.3. Renaming a Package¶
It is likely that the hosting location for a very long-lived package
will change from time to time. Since the hosting location is embedded
into the package path itself, this causes some issues for
futhark pkg
.
In simple cases, there is no problem. Consider a package
github.com/asgard/loki
which is moved to
github.com/utgard/loki
. If no GitHub-level redirect is set up,
all users must update the path by which they import the package. This
is unavoidable, unfortunately.
However, the old tagged versions, which contain a futhark.pkg
that
uses the old package path, will continue to work. This is because the
package path indicated in package.pkg
merely defines the
subdirectory of lib/
where the package files are to be found,
while the package path used by dependents in the require
section
defines where the package files are located after futhark pkg
sync
. Thus, when we import an old version of
github.com/utgard/loki
whose futhark.pkg
defines the package
as github.com/asgard/loki
, the package files will be retrieved
from the lib/github.com/asgard/loki
directory in the repository,
but stored at lib/github.com/utgard/loki
in the local directory.
The above means that package management remains operational in simple
cases of renaming, but it is awkward when a transitive dependency is
renamed (or deleted). The Futhark package ecosystem is sufficiently
embryonic that we have not yet developed more robust solutions. When
such solutions are developed, they will likely involve some form of
replace
directive that allows transparent local renaming of
packages, as well as perhaps a central registry of package paths that
does not depend on specific source code hosts.
6.4. Version Selection¶
The package manifest futhark.pkg
declares which packages the
program depends on. Dependencies are specified as the oldest
acceptable version within the given major version. Upper version
bounds are not supported, as strict adherence to semantic versioning
is assumed, so any later version with the same major version number
should work. When futhark pkg sync
calculates which version of a
given package to download, it will pick the oldest version that still
satisfies the minimum version requirements of that package in all
transitive dependencies. This means that a version may be used that
is newer than the one indicated in futhark.pkg
, but only if a
dependency requires a more recent version.
6.5. Tests and Documentation for Dependencies¶
Package management has been designed to ensure that the normal
development tools work as expected with the contents of the lib/
directory. For example, to ensure that all dependencies do in fact
work well (or at least compile) together, run:
futhark test lib
Also, you can generate hyperlinked documentation for all dependencies with:
futhark doc lib -o docs
The file docs/index.html
can be opened in a web browser to browse
the documentation. Prebuilt documentation is also available via the
online package list.
6.6. Safety¶
In contrast to some other package managers, futhark pkg
does not
run any package-supplied code on installation, upgrade, or removal.
This means that all futhark pkg
operations are in principle
completely safe (barring exploitable bugs in futhark pkg
itself,
which is unlikely but not impossible). Further, Futhark code itself
is also completely pure, so executing it cannot have any unfortunate
effects, such as infecting all of your own packages with a worm. The worst it can do
is loop infinitely, consume arbitrarily large amounts of memory, or
produce wrong results.
The exception is packages that uses unsafe
. With some cleverness,
unsafe
can be combined with in-place updates to perform arbitrary
memory reads and writes, which can trivially lead to exploitable
behaviour. You should not use untrusted code that employs unsafe
(but the --safe
compiler option may help). However, this is not
any worse than calling external code in a conventional impure
language, which generally can perform any conceivable harmful action.
6.7. Private repositories¶
The Futhark package manager is intentionally very simple - perhaps
even simplistic. The key philosophy is that if you can git clone
a repository from the command line, then futhark pkg
can also
access it. However, futhark pkg
always uses the https://
protocol when converting package paths to the URLs that are passed to
git
, which is sometimes inconvenient for self-hosted or private
repositories. As a workaround, you can modify your Git configuration
file to transparently replace https://
with ssh://
for certain
repositories. For example, you can add the following entry
$HOME/.gitconfig
:
[url "ssh://git@github.com/sturluson"]
insteadOf = https://github.com/sturluson
This will make all interactions with repositories owned by the
sturluson
user on GitHub use SSH instead of HTTPS.