diff --git a/source/book.adoc b/source/book.adoc new file mode 100644 index 0000000..4cb44ca --- /dev/null +++ b/source/book.adoc @@ -0,0 +1,28 @@ += Wombat's Book of Nix +{authors}, {build_date} +:sectnums: +:toc: left +:source-highlighter: pygments +:pygments-style: manni +:pygments-linenums-mode: table +:!prewrap: + +include::intro.adoc[] + +include::hello-flake.adoc[] + +include::hello-flake-repo.adoc[] + +include::flake-structure.adoc[] + +include::generic-flake.adoc[] + +include::hello-flake-return.adoc[] + +include::modify-hello-flake.adoc[] + +include::python-flake.adoc[] + +include::shell-recipes/_include.adoc[] + +include::shebangs/_include.adoc[] diff --git a/source/flake-structure.adoc b/source/flake-structure.adoc new file mode 100644 index 0000000..e8a5ff1 --- /dev/null +++ b/source/flake-structure.adoc @@ -0,0 +1,121 @@ +== Flake structure + +The basic structure of a flake is shown below. + +.... +{ + description = ... # package description + inputs = ... # dependencies + outputs = ... # what the flake produces + nixConfig = ... # advanced configuration options +} +.... + +The `description` part is self-explanatory; it’s just a string. You +probably won’t need `nixConfig` unless you’re doing something fancy. I’m +going to focus on what goes into the `inputs` and `outputs` sections, +and highlight some of the things I found confusing. + +=== Inputs + +This section specifies the dependencies of a flake. It’s an _attribute +set_; it maps keys to values. + +To ensure that a build is reproducible, the build step runs in a _pure_ +environment with no network access. Therefore, any external dependencies +must be specified in the ``inputs'' section so they can be fetched in +advance (before we enter the pure environment). + +Each entry in this section maps an input name to a _flake reference_. +This commonly takes the following form. + +.... +NAME.url = URL-LIKE-EXPRESSION +.... + +As a first example, all (almost all?) flakes depend on ``nixpkgs'', +which is a large Git repository of programs and libraries that are +pre-packaged for Nix. We can write that as + +.... +nixpkgs.url = "github:NixOS/nixpkgs/nixos-VERSION"; +.... + +where `NN.MM` is replaced with the version number that you used to build +the package, e.g. `22.11`. Information about the latest nixpkgs releases +is available at https://status.nixos.org/. You can also write the entry +without the version number + +.... +nixpkgs.url = "github:NixOS/nixpkgs/nixos-VERSION"; +.... + +or more simply, + +.... +nixpkgs.url = "nixpkgs"; +.... + +You might be concerned that omitting the version number would make the +build non-reproducible. If someone else builds the flake, could they end +up with a different version of nixpkgs? No! remember that the lockfile +(`flake.lock`) _uniquely_ specifies all flake inputs. + +Git and Mercurial repositories are the most common type of flake +reference, as in the examples below. + +A Git repository:: + `git+https://github.com/NixOS/patchelf` +A specific branch of a Git repository:: + `git+https://github.com/NixOS/patchelf?ref=master` +A specific revision of a Git repository:: + + + `git+https://github.com/NixOS/patchelf?ref=master&rev=f34751b88bd07d7f44f5cd3200fb4122bf916c7e` +A tarball:: + `https://github.com/NixOS/patchelf/archive/master.tar.gz` + +You can find more examples of flake references in the +https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html#examples[Nix +Reference Manual]. + +Although you probably won’t need to use it, there is another syntax for +flake references that you might encounter. This example + +.... +inputs.import-cargo = { + type = "github"; + owner = "edolstra"; + repo = "import-cargo"; +}; +.... + +is equivalent to + +.... +inputs.import-cargo.url = "github:edolstra/import-cargo"; +.... + +Each of the `inputs` is fetched, evaluated and passed to the `outputs` +function as a set of attributes with the same name as the corresponding +input. + +=== Outputs + +This section is a function that essentially returns the recipe for +building the flake. + +We said above that `inputs` are passed to the `outputs`, so we need to +list them as parameters. This example references the `import-cargo` +dependency defined in the previous example. + +.... +outputs = { self, nixpkgs, import-cargo }: { + ... outputs ... +}; +.... + +So what actually goes in this section (where I wrote `...outputs...`)? +That depends on the programming languages your software is written in, +the build system you use, and more. There are Nix functions and tools +that can simplify much of this, and new, easier-to-use ones are released +regularly. We’ll look at some of these in the next section. diff --git a/source/generic-flake.adoc b/source/generic-flake.adoc new file mode 100644 index 0000000..6a575f2 --- /dev/null +++ b/source/generic-flake.adoc @@ -0,0 +1,64 @@ +== A generic flake + +The previous section presented a very high-level view of flakes, +focusing on the basic structure. In this section, we will add a bit more +detail. + +Flakes are written in the Nix programming language, which is a +functional language. As with most programming languages, there are many +ways to achieve the same result. Below is an example you can follow when +writing your own flakes. I’ll explain the example in some detail. + +image:template.png[image] + +We discussed how to specify flake inputs ❶ in the previous section, so +this part of the flake should be familiar. Remember also that any +dependencies in the input section should also be listed at the beginning +of the outputs section ❷. + +Now it’s time to look at the content of the output section. If we want +the package to be available for multiple systems (e.g., +``x86_64-linux'', ``aarch64-linux'', ``x86_64-darwin'', and +``aarch64-darwin''), we need to define the output for each of those +systems. Often the definitions are identical, apart from the name of the +system. The eachDefaultSystem function ❸ provided by flake-utils allows +us to write a single definition using a variable for the system name. +The function then iterates over all default systems to generate the +outputs for each one. + +The `devShells` variable specifies the environment that should be +available when doing development on the package. If you don’t need a +special development environment, you can omit this section. At ❹ you +would list any tools (e.g., compilers and language-specific build tools) +you want to have available in a development shell. If the compiler needs +access to language-specific packages, there are Nix functions to assist +with that. These functions are very language-specific, and not always +well-documented. We will see examples for some languages later in the +tutorial. In general, I recommend that you do a web search for ``nix +language'', and try to find resources that were written or updated +recently. + +The `packages` variable defines the packages that this flake provides. +The package definition ❺ depends on the programming languages your +software is written in, the build system you use, and more. There are +Nix functions and tools that can simplify much of this, and new, +easier-to-use ones are released regularly. Again, I recommend that you +do a web search for ``nix language'', and try to find resources that +were written or updated recently. + +The `apps` variable identifies any applications provided by the flake. +In particular, it identifies the default executable ❻ that `nix run` +will run if you don’t specify an app. + +The list below contains are a few functions that are commonly used in +this section. + +General-purpose:: + The standard environment provides `mkDerivation`, which is especially + useful for the typical `./configure; make; make install` scenario. + It’s customisable. +Python:: + `buildPythonApplication`, `buildPythonPackage`. +Haskell:: + `mkDerivation` (Haskell version, which is a wrapper around the + standard environment version), `developPackage`, `callCabal2Nix`. diff --git a/source/hello-flake-repo.adoc b/source/hello-flake-repo.adoc new file mode 100644 index 0000000..13bec4e --- /dev/null +++ b/source/hello-flake-repo.adoc @@ -0,0 +1,80 @@ +== The hello-flake repo + +Let’s clone the repository and see how the flake is defined. + +.... +$ cd ~/tutorial-practice +$ git clone https://codeberg.org/mhwombat/hello-flake +$ cd hello-flake +$ ls +.... + +This is a simple repo with just a few files. Like most git repos, it +includes `LICENSE`, which contains the software license, and `README.md` +which provides information about the repo. + +The `hello-flake` file is the command we were executing earlier. This +particular executable is just a shell script, so we can view it. It’s an +extremly simple script with just two lines. + +.... +$ cat hello-flake +.... + +Now that we have a copy of the repo, we can execute this script +directly. + +.... +$ ./hello-flake +.... + +Not terribly exciting, I know. But starting with such a simple package +makes it easier to focus on the flake system without getting bogged down +in the details. We’ll make this script a little more interesting later. + +Let’s look at another file. The file that defines how to package a flake +is always called `flake.nix`. + +.... +$ cat flake.nix +.... + +If this is your first time seeing a flake definition, it probably looks +intimidating. Flakes are written in a functional language called +Nixfootnote:[For an introduction to the Nix language, see +https://nixos.org/guides/nix-language.html[Nix language basics].]. Yes, +``Nix'' is the name of both the package manager and the language it +uses. We’ll look at this in more detail shortly. For now, I’d like to +focus on the inputs section. + +.... +inputs = { + nixpkgs.url = "github:NixOS/nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + }; +.... + +There are just two entries, one for `nixpkgs` and one for `flake-utils`. +The first one, `nixpkgs` refers to the collection of standard software +packages that can be installed with the Nix package manager. The second, +`flake-utils`, is a collection of utilities that simplify writing +flakes. The important thing to note is that the `hello-flake` package +_depends_ on `nixpkgs` and `flake-utils`. + +Finally, let’s look at `flake.lock`, or rather, just part of it. + +.... +$ head -n 40 flake.lock +.... + +If `flake.nix` seemed intimidating, then this file looks like an +invocation for Cthulhu. The good news is that this file is automatically +generated; you never need to write it. It contains information about all +of the dependencies for the flake, including where they came from, the +exact version/revision, and hash. This lockfile _uniquely_ specifies all +flake dependencies, (e.g., version number, branch, revision, hash), so +that _anyone, anywhere, any time, can re-create the exact same +environment that the original developer used._ + +No more complaints of ``but it works on my machine!''. That is the +benefit of using flakes. diff --git a/source/hello-flake-return.adoc b/source/hello-flake-return.adoc new file mode 100644 index 0000000..217a736 --- /dev/null +++ b/source/hello-flake-return.adoc @@ -0,0 +1,134 @@ +== Another look at hello-flake + +Now that we have a better understanding of the structure of `flake.nix`, +let’s have a look at the one we saw earlier, in the `hello-flake` repo. +If you compare this flake definition to the colour-coded template +presented in the previous section, most of it should look familiar. + +.... +{ + description = "a very simple and friendly flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + packages = rec { + hello = + . . . + SOME UNFAMILIAR STUFF + . . . + }; + default = hello; + }; + + apps = rec { + hello = flake-utils.lib.mkApp { drv = self.packages.${system}.hello; }; + default = hello; + }; + } + ); +} +.... + +This `flake.nix` doesn’t have a `devShells` section, because development +on the current version doesn’t require anything beyond the ``bare +bones'' linux commands. Later we will add a feature that requires +additional development tools. + +Now let’s look at the section I labeled ```SOME UNFAMILIAR STUFF`'' and +see what it does. + +.... + packages = rec { + hello = pkgs.stdenv.mkDerivation rec { # See (1) in text + name = "hello-flake"; + + src = ./.; # See (2) in text + + unpackPhase = "true"; + + buildPhase = ":"; + + installPhase = + '' + mkdir -p $out/bin # See (3) in text + cp $src/hello-flake $out/bin/hello-flake # See (4) in text + chmod +x $out/bin/hello-flake # See (5) in text + ''; + }; +.... + +This flake uses `mkDerivation` (1) which is a very useful +general-purpose package builder provided by the Nix standard +environment. It’s especially useful for the typical +`./configure; make; make install` scenario, but for this flake we don’t +even need that. + +The `name` variable is the name of the flake, as it would appear in a +package listing if we were to add it to Nixpkgs or another package +collection. The `src` variable (2) supplies the location of the source +files, relative to `flake.nix`. When a flake is accessed for the first +time, the repository contents are fetched in the form of a tarball. The +`unpackPhase` variable indicates that we do want the tarball to be +unpacked. + +The `buildPhase` variable is a sequence of Linux commands to build the +package. Typically, building a package requires compiling the source +code. However, that’s not required for a simple shell script. So +`buildPhase` consists of a single command, `:`, which is a no-op or ``do +nothing'' command. + +The `installPhase` variable is a sequence of Linux commands that will do +the actual installation. In this case, we create a directory (3) for the +installation, copy the `hello-flake` script (4) there, and make the +script executable (5). The environment variable `$src` refers to the +source directory, which we specified earlier (2). + +Earlier we said that the build step runs in a pure environment to ensure +that builds are reproducible. This means no Internet access; indeed no +access to any files outside the build directory. During the build and +install phases, the only commands available are those provided by the +Nix standard environment and the external dependencies identified in the +`inputs` section of the flake. + +I’ve mentioned the Nix standard environment before, but I didn’t explain +what it is. The standard environment, or `stdenv`, refers to the +functionality that is available during the build and install phases of a +Nix package (or flake). It includes the commands listed +belowfootnote:[For more information on the standard environment, see the +https://nixos.org/manual/nixpkgs/stable/#sec-tools-of-stdenv[Nixpkgs +manual]]. + +* The GNU C Compiler, configured with C and C++ support. +* GNU coreutils (contains a few dozen standard Unix commands). +* GNU findutils (contains find). +* GNU diffutils (contains diff, cmp). +* GNU sed. +* GNU grep. +* GNU awk. +* GNU tar. +* gzip, bzip2 and xz. +* GNU Make. +* Bash. +* The patch command. +* On Linux, stdenv also includes the patchelf utility. + +Only a few environment variables are available. The most interesting +ones are listed below. + +* `$name` is the package name. +* `$src` refers to the source directory. +* `$out` is the path to the location in the Nix store where the package +will be added. +* `$system` is the system that the package is being built for. +* `$PWD` and `$TMP` both point to a temporary build directories +* `$HOME` and `$PATH` point to nonexistent directories, so the build +cannot rely on them. diff --git a/source/hello-flake.adoc b/source/hello-flake.adoc new file mode 100644 index 0000000..3a65c35 --- /dev/null +++ b/source/hello-flake.adoc @@ -0,0 +1,53 @@ +== Hello, flake! + +Before learning to write Nix flakes, let’s learn how to use them. I’ve +created a simple example of a flake in this git +https://codeberg.org/mhwombat/hello-flake[repository]. To run this +flake, you don’t need to install anything; simply run the command below. +The first time you use a package, Nix has to fetch and build it, which +may take a few minutes. Subsequent invocations should be instantaneous. + +.... +$ nix run "git+https://codeberg.org/mhwombat/hello-flake" +.... + +That’s a lot to type every time we want to use this package. Instead, we +can enter a shell with the package available to us, using the +`nix shell` command. + +.... +$ nix shell "git+https://codeberg.org/mhwombat/hello-flake" +.... + +In this shell, the command is on our `$PATH`, so we can execute the +command by name. + +.... +$ hello-flake +.... + +Nix didn’t _install_ the package; it merely built and placed it in a +directory called the ``Nix store''. Thus we can have multiple versions +of a package without worrying about conflicts. We can find out the +location of the executable, if we’re curious. + +.... +$ which hello-flake +.... + +Once we exit that shell, the `hello-flake` command is no longer +available. + +.... +$# echo '$ exit' +$# echo '$ hello-flake' +sh: line 3: hello-flake: command not found +.... + +Actually, we can still access the command using the store path we found +earlier. That’s not particularly convenient, but it does demonstrate +that the package remains in the store for future use. + +.... +/nix/store/0xbn2hi6h1m5h4kc02vwffs2cydrbc0r-hello-flake/bin/hello-flake +.... diff --git a/source/intro.adoc b/source/intro.adoc new file mode 100644 index 0000000..3d4eea8 --- /dev/null +++ b/source/intro.adoc @@ -0,0 +1,61 @@ +== Introduction + +=== Why Nix? + +If you’ve opened this PDF, you already have your own motivation for +learning Nix. Here’s how it helps me. As a researcher, I tend to work on +a series of short-term projects, mostly demos and prototypes. For each +one, I typically develop some software using a compiler, often with some +open source libraries. Often I use other tools to analyse data or +generate documentation, for example. + +Problems would arise when handing off the project to colleagues; they +would report errors when trying to build or run the project. Belatedly I +would realise that my code relies on a library that they need to +install. Or perhaps they had installed the library, but the version +they’re using is incompatible. + +Using containers helped with the problem. However, I didn’t want to +_develop_ in a container. I did all my development in my nice, familiar, +environment with my custom aliases and shell prompt. and _then_ I +containerised the software. This added step was annoying for me, and if +my colleague wanted to do some additional development, they would +probably extract all of the source code from the container first anyway. +Containers are great, but this isn’t the ideal use case for them. + +Nix allows me to work in my custom environment, but forces me to specify +any dependencies. It automatically tracks the version of each dependency +so that it can replicate the environment wherever and whenever it’s +needed. + +=== Why _flakes_? + +Flakes are labeled as an experimental feature, so it might seem safer to +avoid them. However, they have been in use for years, and there is +widespread adoption, so the aren’t going away any time soon. Flakes are +easier to understand, and offer more features than the traditional Nix +approach. After weighing the pros and cons, I feel it’s better to learn +and use flakes; and this seems to be the general consensus. + +=== Prerequisites + +To follow along with this tutorial, you will need access to a computer +or (virtual machine) with Nix installed and _flakes enabled_. + +I recommend the installer from +https://zero-to-nix.com/start/install[zero-to-nix.com]. This installer +automatically enables flakes. + +More documentation (and another installer) available at +https://nixos.org/[nixos.org]. + +To enable flakes on an existing installation, see the instructions in +the https://nixos.wiki/wiki/Flakes[NixOS wiki]. + +=== Tip: Pay attention to those hyphens + +There are hyphenated and unhyphenated versions of many Nix commands. For +example, `nix-shell` and `nix shell` are two different commands. + +Generally speaking, the unhyphenated versions are for working directly +with flakes, while the hyphenated versions are for everything else. diff --git a/source/modify-hello-flake.adoc b/source/modify-hello-flake.adoc new file mode 100644 index 0000000..ef9747e --- /dev/null +++ b/source/modify-hello-flake.adoc @@ -0,0 +1,201 @@ +== Modifying the flake + +Let’s make a simple modification to the script. This will give you an +opportunity to check your understanding of flakes. + +The first step is to enter a development shell. + +.... +$ cd ~/tutorial-practice/hello-flake +$ nix develop +.... + +The `flake.nix` file specifies all of the tools that are needed during +development of the package. The `nix develop` command puts us in a shell +with those tools. As it turns out, we didn’t need any extra tools +(beyond the standard environment) for development yet, but that’s +usually not the case. Also, we will soon need another tool. + +A development environment only allows you to _develop_ the package. +Don’t expect the package _outputs_ (e.g. executables) to be available +until you build them. However, our script doesn’t need to be compiled, +so can’t we just run it? + +.... +$ hello-flake +.... + +That worked before; why isn’t it working now? Earlier we used +`nix shell` to enter a _runtime_ environment where `hello-flake` was +available and on the `$PATH`. This time we entered a _development_ +environment using the `nix develop` command. Since the flake hasn’t been +built yet, the executable won’t be on the `$PATH`. We can, however, run +it by specifying the path to the script. + +.... +$ ./hello-flake +.... + +We can also build the flake using the `nix build` command, which places +the build outputs in a directory called `result`. + +.... +$ nix build +$ result/bin/hello-flake +.... + +Rather than typing the full path to the executable, it’s more convenient +to use `nix run`. + +.... +$ nix run +.... + +Here’s a summary of the more common Nix commands. + +[width="100%",cols="<17%,<83%",options="header",] +|=== +|command |Action +|`nix develop` |Enters a _development_ shell with all the required +development tools (e.g. compilers and linkers) available (as specified +by `flake.nix`). + +|`nix shell` |Enters a _runtime_ shell where the flake’s executables are +available on the `$PATH`. + +|`nix build` |Builds the flake and puts the output in a directory called +`result`. + +|`nix run` |Runs the flake’s default executable, rebuilding the package +first if needed. Specifically, it runs the version in the Nix store, not +the version in `result`. +|=== + +Now we’re ready to make the flake a little more interesting. Instead of +using the `echo` command in the script, we can use the Linux `cowsay` +command. The `sed` command below will make the necessary changes. + +.... +$ sed -i 's/echo/cowsay/' hello-flake +$ cat hello-flake +.... + +Let’s test the modified script. + +.... +$ ./hello-flake +.... + +What went wrong? Remember that we are in a _development_ shell. Since +`flake.nix` didn’t define the `devShells` variable, the development +shell only includes the Nix standard environment. In particular, the +`cowsay` command is not available. + +To fix the problem, we can modify `flake.nix`. We don’t need to add +`cowsay` to the `inputs` section because it’s included in `nixpkgs`, +which is already an input. However, we do need to indicate that we want +it available in a develoment shell. Add the following lines before the +`packages = rec {` line. + +.... + devShells = rec { + default = pkgs.mkShell { + packages = [ pkgs.cowsay ]; + }; + }; +.... + +Here is a ``diff'' showing the changes in `flake.nix`. + +.... +$# sed -i '15i\\ \ \ \ \ \ \ \ devShells = rec {\n\ \ \ \ \ \ \ \ \ \ default = pkgs.mkShell {\n\ \ \ \ \ \ \ \ \ \ \ \ packages = [ pkgs.cowsay ];\n\ \ \ \ \ \ \ \ \ \ };\n\ \ \ \ \ \ \ \ };\n' flake.nix +$ git diff flake.nix +.... + +We restart the development shell and see that the `cowsay` command is +now available and the script works. Because we’ve updated source files +but haven’t `git commit`ed the new version, we get a warning message +about it being ``dirty''. It’s just a warning, though; the script runs +correctly. + +.... +$# echo '$ nix develop' +$# nix develop --command sh +$ which cowsay # is it available now? +$ ./hello-flake +.... + +Alternatively, we could use `nix run`. + +.... +$ nix run +.... + +Note, however, that `nix run` rebuilt the package in the Nix store and +ran _that_. It did not alter the copy in the `result` directory, as +we’ll see next. + +.... +$ cat result/bin/hello-flake +.... + +If we want to update the version in `result`, we need `nix build` again. + +.... +$ nix build +$ cat result/bin/hello-flake +.... + +Let’s `git commit` the changes and verify that the warning goes away. We +don’t need to `git push` the changes until we’re ready to share them. + +.... +$ git commit hello-flake flake.nix -m 'added bovine feature' +$ nix run +.... + +=== Development workflows + +If you’re getting confused about when to use the different commands, +it’s because there’s more than one way to use Nix. I tend to think of it +as two different development workflows. + +My usual, _high-level workflow_ is quite simple. + +[arabic] +. `nix run` to re-build (if necessary) and run the executable. +. Fix any problems in `flake.nix` or the source code. +. Repeat until the package works properly. + +In the high-level workflow, I don’t use a development shell because I +don’t need to directly invoke development tools such as compilers and +linkers. Nix invokes them for me according to the output definition in +`flake.nix`. + +Occasionally I want to work at a lower level, and invoke compiler, +linkers, etc. directly. Perhaps want to work on one component without +rebuilding the entire package. Or perhaps I’m confused by some error +message, so I want to temporarily bypass Nix and ``talk'' directly to +the compiler. In this case I temporarily switch to a _low-level +workflow_. + +[arabic] +. `nix develop` to enter a development shell with any development tools +I need (e.g. compilers, linkers, documentation generators). +. Directly invoke tools such as compilers. +. Fix any problems in `flake.nix` or the source code. +. Directly invoke the executable. Note that the location of the +executable depends on the development tools – It probably isn’t +`result`! +. Repeat until the package works properly. + +I generally only use `nix build` if I just want to build the package but +not execute anything (perhaps it’s just a library). + +=== This all seems like a hassle! + +It is a bit annoying to modify `flake.nix` and ether rebuild or reload +the development environment every time you need another tool. However, +this Nix way of doing things ensures that all of your dependencies, down +to the exact versions, are captured in `flake.lock`, and that anyone +else will be able to reproduce the development environment. diff --git a/source/python-flake.adoc b/source/python-flake.adoc new file mode 100644 index 0000000..a42867e --- /dev/null +++ b/source/python-flake.adoc @@ -0,0 +1,194 @@ +== A new flake from scratch (Python) + +At last we are ready to create a flake from scratch! Start with an empty +directory and create a git repository. + +.... +$ cd ~/tutorial-practice +$ mkdir hello-python +$ cd hello-python +$ git init +.... + +Next, we’ll create a simple Python program. + +.... +$# curl https://codeberg.org/mhwombat/hello-flake-python/raw/branch/main/hello.py --silent --output hello.py +$ cat hello.py +.... + +Before we pachage the program, let’s verify that it runs. We’re going to +need Python. By now you’ve probably figured out that we can write a +`flake.nix` and define a development shell that includes Python. We’ll +do that shortly, but first I want to show you a handy shortcut. We can +lauch a _temporary_ shell with any Nix packages we want. This is +convenient when you just want to try out some new software and you’re +not sure if you’ll use it again. It’s also convenient when you’re not +ready to write `flake.nix` (perhaps you’re not sure what tools and +packages you need), and you want to experiment a bit first. + +The command to enter a temporary shell is + +`nix-shell -p` _packages_ + +If there are multiple packages, they should be separated by spaces. Note +that the command used here is `nix-shell` with a hyphen, not `nix shell` +with a space; those are two different commands. In fact there are +hyphenated and non-hyphenated versions of many Nix commands, and yes, +it’s confusing. The non-hyphenated commands were introduced when support +for flakes was added to Nix. I predict that eventually all hyphenated +commands will be replaced with non-hyphenated versions. Until then, a +useful rule of thumb is that non-hyphenated commands are for for working +directly with flakes; hyphenated commands are for everything else. + +Let’s enter a shell with Python so we can test the program. + +.... +$# echo '$ nix-shell -p python3' +$# nix-shell -p python3 --command sh +$ python hello.py +.... + +Next, create a Python script to build the package. We’ll use Python’s +setuptools, but you can use other build tools. For more information on +setuptools, see the +https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/[Python +Packaging User Guide], especially the section on +https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#setup-args[setup +args]. + +.... +$# curl https://codeberg.org/mhwombat/hello-flake-python/raw/branch/main/setup.py --silent --output setup.py +$ cat setup.py +.... + +We won’t write `flake.nix` just yet. First we’ll try building the +package manually. + +.... +$ python -m build +.... + +The missing module error happens because we don’t have `build` available +in the temporary shell. We can fix that by adding ``build'' to the +temporary shell. When you need support for both a language and some of +its packages, it’s best to use one of the Nix functions that are +specific to the programming language and build system. For Python, we +can use the `withPackages` function. + +.... +$# echo '$ nix-shell -p "python3.withPackages (ps: with ps; [ build ])"' +$# nix-shell -p "python3.withPackages (ps: with ps; [ build ])" --command sh +.... + +Note that we’re now inside a temporary shell inside the previous +temporary shell! To get back to the original shell, we have to `exit` +twice. Alternatively, we could have done `exit` followed by the +`nix-shell` command. + +.... +$# echo '$ python -m build' +$# python -m build > /dev/null 2>&1 +.... + +After a lot of output messages, the build succeeds. + +Now we should write `flake.nix`. We already know how to write most of +the flake from the examples we did earlier. The two parts that will be +different are the development shell and the package builder. + +Let’s start with the development shell. It seems logical to write +something like the following. + +.... + devShells = rec { + default = pkgs.mkShell { + packages = [ (python.withPackages (ps: with ps; [ build ])) ]; + }; + }; +.... + +Note that we need the parentheses to prevent `python.withPackages` and +the argument from being processed as two separate tokens. Suppose we +wanted to work with `virtualenv` and `pip` instead of `build`. We could +write something like the following. + +.... + devShells = rec { + default = pkgs.mkShell { + packages = [ + # Python plus helper tools + (python.withPackages (ps: with ps; [ + virtualenv # Virtualenv + pip # The pip installer + ])) + ]; + }; + }; +.... + +For the package builder, we can use the `buildPythonApplication` +function. + +.... + packages = rec { + hello = python.pkgs.buildPythonApplication { + name = "hello-flake-python"; + buildInputs = with python.pkgs; [ pip ]; + src = ./.; + }; + default = hello; + }; +.... + +If you put all the pieces together, your `flake.nix` should look +something like this. + +.... +$# curl https://codeberg.org/mhwombat/hello-flake-python/raw/branch/main/flake.nix --silent --output flake.nix +$ cat flake.nix +.... + +Let’s try out the new flake. + +.... +$ nix run +.... + +Why can’t it find `flake.nix`? Nix flakes only ``see'' files that are +part of the repository. We need to add all of the important files to the +repo before building or running the flake. + +.... +$ git add flake.nix setup.py hello.py +$ nix run +.... + +We’d like to share this package with others, but first we should do some +cleanup. When the package was built (automatically by the `nix run` +command), it created a `flake.lock` file. We need to add this to the +repo, and commit all important files. + +.... +$ git add flake.lock +$ git commit -a -m 'initial commit' +.... + +You can test that your package is properly configured by going to +another directory and running it from there. + +.... +$ cd ~ +$ nix run ~/tutorial-practice/hello-python +.... + +If you move the project to a public repo, anyone can run it. Recall from +the beginning of the tutorial that you were able to run `hello-flake` +directly from my repo with the following command. + +.... +nix run "git+https://codeberg.org/mhwombat/hello-flake" +.... + +Modify the URL accordingly and invite someone else to run your new +Python flake.