4 minutes
How I do software
I want to write down how like to deal with projects in software development. These things might be obvious to some, a debatable point for others and blasphemy to many.
Source code
The basic unit of software is a directory of source code. Preferrably, this is git repository, so the code is versioned and can be rolled-back, and worked on concurrently with other team members.
It should not contain binaries for any tools or testing, because any file in here is meant to be understood.
There can be “excluded” sub directories in the file system that contain build artifacts, but they are never checked into source control, so they are not really in the repository.
In the root directory, there should be a README.md, which describes what this project is, how it can be used and how it works.
I prefer the code to be formatted with a code formatter. Code carries semantic informataion, which is unaffected by the exact syntax of the code. I want to write sloppily formatted code and then rely on the formatter to make it pretty and uniform. And exact style does not matter - you will get used to it. It only matters that it is uniform over the whole codebase.
Development shell
Then there is the development shell, by which I mean a collection of tools needed to work with the project. This includes any compilers, interpreters, libraries, language servers, test frameworks or build tools. Traditionally, this part is specified in README.md or CONTRIBING.md as a list of things needed. I prefer a programatic apprach and my current favourite is a Nix Flake. It would look like this:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.cargo
pkgs.rustc
pkgs.clippy
pkgs.rustfmt
pkgs.rust-analyzer
pkgs.just
pkgs.cargo-nextest
pkgs.cargo-insta
];
};
}
);
}
This example specifies that we need the rust toolchain, rust-analyzer, just and a few testing cargo crates.
That’s right, I include language servers and code formatters in the “dev shell”. They should not be provided by the IDE, but specified by the code directory. That’s because the version of these tools might be important or the team working on this codebase might change which tools are being used (for example, code foramtters).
I’m not set on using Nix flakes, but I do value following properties of dev shells:
- shell can be built on any machine, regardless of Linux/Mac distribution,
- shell can be reproduced with exactly same tool versions, now or anytime in the (not so distant) future,
- shells can easily be modified and updated,
I don’t like depending on any tools from the host machine, even if “everyone has them installed”.
Even if everyone has make or jq or grep, I prefer to specify this explictly.
This also helps with providing environment in which CI can run.
I very much don’t like adding the tools into the repository directly (looking at you gradlew).
If every tool would be doing this, it would be a mess.
I don’t like toolchain managers for one specific language, (such as rustup or nvm), because they cannot deliver all tools needed.
Another thing I like to use is direnv, which loads the
dev shell when I cd into the code directory. It reads the .envrc file that
contains use flake directive and then invokes nix to load the shell.
Entrypoint
To get started with development easily, I like to have file that describes commands to run when work with the codebase.
Generally, this includes building, testing, linting or formatting.
Traditionally, this was described in README.md as a set of codeblocks that
one would copy-paste into the command line. I prefer using just, which is a
simplified make that just runs commands. In the root of project, it would have
such justfile:
_default:
just --list
test:
cargo nextest run --no-fail-fast
cargo insta review
fmt:
cargo fmt
generate:
lutra codegen lutra/ ./src/generated.rs
cargo fmt -p lutra-bin
pull-request: generate fmt test
echo 'Ok'
This contains the following recipes:
just test, which runs the tests,just fmt, which formats the codebase,just generate, which generates some code,just pull-request, which can run in CI on each pull request,- and the default command that just lists all available commands.
It can get more comlicated, but it should cover most of what people are running in day-to-day workflows.
Editors
Notably, I do not count code editors into the “development shell”. That’s because the editor is a very personal choice and cannot be unified over all code contributors.
Also, any config files & directories (.vscode/, .helix/) should be excluded,
since developers will probably not want to have exactly the same editor config.
There can be templates for language servers, or other useful options one might
want to get started with.
810 Words
2025-08-02 (Last updated: 2025-11-06 10:18 +00:00)