Skip to content

cachix/nixcon-2024-workshop

Repository files navigation

NixCon 2024

Devenv workshop

During the workshop, we'll try to create a development environment for a real-world Rust project with devenv.

https://devenv.sh/

Prerequisites

Here's what you'll need for this workshop:

  • git
  • nix
  • devenv
  • direnv (optional)

If you haven't yet installed Nix or devenv, follow the instructions for your platform on https://devenv.sh/getting-started/.

Set up a GitHub token

To avoid getting rate-limited by GitHub, we recommend providing Nix with a GitHub API token.

Create a new token with no extra permissions at https://github.com/settings/personal-access-tokens/new Add the token to your ~/.config/nix/nix.conf:

access-tokens = github.com=<GITHUB_TOKEN>

Clone the repo

git clone https://github.com/cachix/nixcon-2024-workshop && cd nixcon-2024-workshop

Explore the README

Projects out in the wild will often list their dependencies, setup steps, and other useful information in the README.md. This repo has a PROJECT_README.md that lists these requirements.

We're going to use the ad-hoc instructions to build up a developer environment powered by Nix.

Initialize devenv

Lets create the initial devenv files:

devenv init

This will create the following two files:

  • devenv.nix: used to specify your developer environment in Nix.
  • devenv.yaml: lets you specify dependencies on other source repositories, flakes, etc, much in the style in flake inputs.

If you have direnv installed, the shell will automatically start loading after this command. You can also manually load the environment with:

devenv shell
Running tasks     devenv:enterShell
Succeeded         devenv:enterShell 10ms
1 Succeeded                         10.47ms

hello from devenv
git version 2.44.0

Lets remove the default configuration and start from scratch.

{ pkgs, lib, config, inputs, ... }:

{
-  # https://devenv.sh/basics/
-  env.GREET = "devenv";
-
-  # https://devenv.sh/packages/
-  packages = [ pkgs.git ];
-
-  # https://devenv.sh/languages/
-  # languages.rust.enable = true;
-
-  # https://devenv.sh/processes/
-  # processes.cargo-watch.exec = "cargo-watch";
-
-  # https://devenv.sh/services/
-  # services.postgres.enable = true;
-
-  # https://devenv.sh/scripts/
-  scripts.hello.exec = ''
-    echo hello from $GREET
-  '';
-
-  enterShell = ''
-    hello
-    git --version
-  '';
-
-  # https://devenv.sh/tasks/
-  # tasks = {
-  #   "myproj:setup".exec = "mytool build";
-  #   "devenv:enterShell".after = [ "myproj:setup" ];
-  # };
-
-  # https://devenv.sh/tests/
-  enterTest = ''
-    echo "Running tests"
-    git --version | grep --color=auto "${pkgs.git.version}"
-  '';
-
-  # https://devenv.sh/pre-commit-hooks/
-  # pre-commit.hooks.shellcheck.enable = true;
-
-  # See full reference at https://devenv.sh/reference/options/
}

Dotenv support

You may have noticed a notification that devenv has detected a .env file in the project. This file contains environment variables that are used to configure the project.

We can load them into our environment with the dotenv integration:

{ pkgs, lib, config, inputs, ... }:

{
+  dotenv.enable = true;
}
$ echo $DATABASE_URL
postgres://localhost:5431/flakestry

Language support

We know from the README that we'll need rust for the backend, and javascript/typescript and elm for the frontend. Lets enable these languages in the devenv.nix file.

{ pkgs, lib, config, inputs, ... }:

{
+ languages.rust.enable = true;
+
+ languages.javascript = {
+   enable = true;
+   npm.install.enable = true;
+ };
+
+ languages.typescript.enable = true;
+
+ languages.elm.enable = true;
}

Tip

Some languages support more extensive versioning support than what is available in nixpkgs.

For example, the Rust integration supports using a specific channel or an entirely custom toolchain.

- languages.rust.enable = true;
+ languages.rust = {
+   enable = true;
+   channel = "stable";
+ };

This feature uses nix-community/fenix under the hood. devenv will prompt you do add it as an input to your devenv.yaml. You can do so throught the command-line:

devenv inputs add fenix github:nix-community/fenix --follows nixpkgs

Services

This project relies on 3 main services:

  • PostgreSQL as the main database.
  • OpenSearch for indexing and searching for releases.
  • Caddy as a reverse proxy for the frontend and backend.

Lets enable these services in the devenv.nix file.

languages.typescript.enable = true;

languages.elm.enable = true;
+
+ services.caddy.enable = true;
+ services.caddy.config = builtins.readFile ./Caddyfile;
+
+ services.opensearch.enable = true;
+
+ services.postgres = {
+   enable = true;
+   listen_addresses = "localhost";
+   port = 5432;
+   initialDatabases = [ { name = "flakestry"; } ];
+ };

Launch the services with:

devenv up

devenv will configure and launch the processes in an interactive process manager. By default, this is process-compose, but we support several other implementations via process.manager.implementation.

To bring down the processes, use Ctrl+C + ENTER or run devenv processes down in another terminal (in the same directory).

Scripts

You can also define custom bash scripts in devenv.nix.

+ scripts.run-migrations.exec = "sqlx migrate run";

Scripts are available in the devenv shell by name. With postgres running, we can now run the migrations:

run-migrations

Custom processes

Now that we've set up our services, we can start adding custom processes for our backend and frontend. We'll also add a few extra packages to our shell.

services.postgres = {
  enable = true;
  listen_addresses = "localhost";
  port = 5432;
  initialDatabases = [ { name = "flakestry"; } ];
};
+
+ packages = [
+   pkgs.openssl
+   pkgs.sqlx-cli
+   pkgs.cargo-watch
+   pkgs.elmPackages.elm-land
+ ];
+
+ processes.backend.exec = "cd backend && cargo watch -x run";
+ processes.frontend.exec = "cd frontend && elm-land server"

Tip

For macOS users, the Rust backend requires a few macOS frameworks.

   pkgs.elmPackages.elm-land
+ ]
+ # For macOS machines
| ++ lib.optionals pkgs.stdenv.isDarwin [
+   pkgs.darwin.CF
+   pkgs.darwin.Security
+   pkgs.darwin.configd
+   pkgs.darwin.dyld
+ ];

The backend process might fail to initialize properly if the opensearch cluster is not ready at the time of launch.

We can leverage the depends_on feature of process-compose to record this runtime ordering. This will ensure that the backend process only starts after the opensearch and postgres services are healthy.

- processes.backend.exec = "cd backend && cargo watch -x run";
+ processes.backend = {
+   exec = "cd backend && cargo watch -x run";
+   process-compose.depends_on = {
+     opensearch.condition = "process_healthy";
+     postgres.condition = "process_healthy";
+   };
+ };
+

You should now have a fully working development environment to run Flakestry!

Go to http://localhost:8888 to see it working.

There's an app in the repo to test out publishing to Flakestry. Lets add it as a script.

+ scripts.flakestry-publish.exec = "cd backend && cargo run --bin publish -- $@";

Create a GitHub token with no extra permissions: https://github.com/settings/personal-access-tokens/new

export GITHUB_TOKEN=your-token

or if you have gh installed:

export GITHUB_TOKEN=$(gh auth token)

Let's try it out:

flakestry-publish --owner nixos --repo nixpkgs --version 24.05

Final devenv.nix

{ pkgs, lib, config, inputs, ... }:
{
  dotenv.enable = true;

  packages =
    [
      pkgs.openssl
      pkgs.cargo-watch
      pkgs.elmPackages.elm-land
      pkgs.sqlx-cli
    ]
    # For macOS machines
    ++ lib.optionals pkgs.stdenv.isDarwin [
      pkgs.darwin.CF
      pkgs.darwin.Security
      pkgs.darwin.configd
      pkgs.darwin.dyld
    ];

  languages.rust = {
    enable = true;
    channel = "stable";
  };

  languages.javascript = {
    enable = true;
    npm.install.enable = true;
  };

  languages.typescript.enable = true;

  languages.elm.enable = true;

  services.caddy.enable = true;
  services.caddy.config = builtins.readFile ./Caddyfile;

  services.opensearch.enable = true;

  services.postgres = {
    enable = true;
    listen_addresses = "localhost";
    port = 5431;
    initialDatabases = [ { name = "flakestry"; } ];
  };

  scripts.run-migrations.exec =  "sqlx migrate run";
  scripts.flakestry-publish.exec = "cd backend && cargo run --bin publish -- $@";

  processes = {
    backend = {
      exec = "cd backend && cargo watch -x run";
      process-compose.depends_on = {
        opensearch.condition = "process_healthy";
        postgres.condition = "process_healthy";
      };
    };
    frontend.exec = "cd frontend && elm-land server";
  };

  pre-commit = {
    hooks = {
      rustfmt.enable = true;
      rustfmt.packageOverrides.rustfmt = config.languages.rust.toolchain.rustfmt;

      nixfmt-rfc-style.enable = true;

      elm-format.enable = true;
    };
    settings.rust.cargoManifestPath = "./backend/Cargo.toml";
  };
}

Contributing

Join us on Discord if you have questions, thoughts, or suggestions.

If you find bugs, open an issue on https://github.com/cachix/devenv/issues.