Alle programmer har avhengigheter. Og de er gjerne veldig spesifikke. Backenden din krever kanskje en bestemt versjon av Java, Maven, Redis og PostgreSQL. Frontenden din krever helt sikkert en bestemt versjon av node og npm. Hvis du er heldig så har du kanskje fått noen script fra et plattformteam som krever bestemte utgaver av GNU sed og awk.

Disse avhengighetene må ikke bare være tilgjengelig på din maskin uansett om du bruker Windows, Mac eller Linux, de bør helst være identisk det som kjører i CI-miljøet og i produksjon.

Hvordan sørger vi for at alle utviklerne bruker de samme avhengighetene, og hvordan holder vi det synkronisert på tvers av alle miljøer?

I de fleste prosjekter jeg har jobbet på så vil det finnes en virtuell maskin eller et docker image for ting som databaser og cacher. Det varierer hvordan avhengigheter som språk eller mindre programmer som brukes i script håndteres. Her har jeg sett alt fra asdf, sdkman og instrukser i en Readme.md fil. Som oftest må CI-miljøet holdes oppdatert manuelt.

Docker er i utgangspunktet bra, men det har sine ulemper, spesielt hvis du bruker det til mange avhengigheter. På Mac og Windows kjører Docker i en virtuell maskin, siden det tross alt er avhengig av Linux-spesifikk funksjonalitet. Dette gjør at det ikke kjører med optimal ytelse. Din maskin og den virtuelle maskinen har hvert sitt nettverk, som av og til er en kompliserende faktor når folk bruker forskjellige operativsystem, for Linux-brukere kjører ikke Docker i en virtuell maskin.

Siden hvert Docker image potensielt bygger opp et helt OS, så bruker det gjerne mye plass. Det har hendt meg flere ganger at Docker har brukt 40+ GB med plass på harddisken. Det er ganske mye for avhengigheter som isolert sett krever ~200MB lagring.

Docker løser helt klart et problem, men det kommer på bekostning av ytelse, lagring og kompleksitet.

Finnes det ingen bedre løsninger?

Nix

Dette er et pakkehåndteringssystem jeg har fulgt med på en stund. Det som er unikt med Nix er at du kan definere alle avhengighetene du trenger, med spesifikke versjonsnummer, i en fil. Denne fila kan sjekkes inn i Git eller Fossil eller hvilket som helst versjonskontrollsystem slik at du har en backup av avhengighetene dine.

Nix har også en interessant egenskap som gjør at du kan definere avhengigheter på prosjektbasis. Du kan for eksempel ha en fil som definerer globale avhengigheter (avhengigheter du ønsker alltid skal være tilgjengelig) som f.eks. tekst-editor, hjelpeverktøy, standardversjon av Java osv. Du kan i tillegg ha en fil som definerer prosjektavhengigheter, som da kan overstyre eventuelle globale avhengigheter. Dette betyr at du kan inkludere Nix-fila i prosjektet ditt, og så lenge alle bruker Nix så vil alle bruke nøyaktig de samme avhengighetene.

Nix kjører ikke i en virtuell maskin, så dette blir så effektivt som det er mulig å bli.

Det finnes til og med et eget operativsystem som er bygd rundt Nix, kalt NixOS. Her er konseptet dratt så langt som mulig, og du kan bestemme absolutt alle avhengigheter med én enkelt fil.

Problemet? Nix bruker et eget språk for å definere avhengigheter som er overraskende komplisert. Hovedgrunnen til at jeg ikke bruker Nix er at jeg ikke finner tid til å lære meg språket.

Heldigvis finnes det en løsning.

Devbox

Hvis jeg skal oppsummere Devbox med en setning så vil det være at det gir deg fordelene med Nix med kompleksiteten til NPM.

Devbox er en alternativ frontend til Nix, så det er faktisk Nix som brukes. Du bare slipper unna Nix-språket.

Dette er devbox.json fila som brukes for kompilatoren til Gren:

{
  "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.4/.schema/devbox.schema.json",
  "packages": [
    "nodejs@20",
    "ghc@9.4.8",
    "cabal-install@3.8.1.0",
    "ormolu@0.7"
  ],
  "shell": {
    "init_hook": [
      "echo 'Welcome to devbox!' > /dev/null"
    ],
    "scripts": {
      "format": [
        "ormolu --mode inplace $(git ls-files '*.hs')",
        "npm run prettier"
      ],
      "format:check": [
        "ormolu --check-idempotence --mode check $(git ls-files '*.hs')",
        "npm run prettier:check"
      ],
      "build": [
        "./build_dev_bin.sh",
        "npm run prepublishOnly"
      ],
      "test": [
        "cabal test"
      ]
    }
  }
}

Avhengighetene er spesifisert under ”packages”. Vanlige kommandoer er definert under ”scripts”. Disse kan du kjøre med devbox run <script_name>, og de vil da kjøres med de definerte avhengighetene.

Kanskje mer interessant er at du kan kjøre devbox shell, og du vil da få en terminal hvor avhengighetene du har spesifisert er lagt til i PATH, som om de var installert globalt på maskinen din.

Si at vi skal skrive et bygg-script for prosjektet og ønsker å benytte oss av kommandolinjeverktøyet jq for å redigere noen .json filer. jq kan legges til ved hjelp av devbox add jq. Ferdig.

Hva med en database? La oss si at vi av en eller annen grunn trenger akkurat versjon 13 av postgres, er det mulig?

Vi kan søke opp hvilke versjoner av postgres som er tilgjengelig ved å kjøre devbox search postgres, som gir oss:

Found 25+ results for "postgres":

* postgres-lsp  (0-unstable-2024-03-24, 2024-01-11, 2023-10-20, 2023-09-21, 2023-08-23, 2023-08-08)
* gnatcoll-postgres  (24.0.0, 23.0.0, 22.0.0, 21.0.0)
* prometheus-postgres-exporter  (0.15.0, 0.14.0, 0.13.2, 0.13.1, 0.13.0, 0.12.1, 0.12.0, 0.11.1, 0.11.0, 0.10.1 ...)
* postgresql  (16.4, 15.7, 15.6, 15.5, 15.4, 14.9, 14.8, 14.7, 14.6, 14.5 ...)
* postgresql95  (9.5.25, 9.5.24, 9.5.23, 9.5.22, 9.5.21)
* postgresql96  (9.6.24, 9.6.23, 9.6.22, 9.6.21, 9.6.20, 9.6.19, 9.6.18, 9.6.17)
* postgresqlTestHook
* postgrest  (12.0.2)
* postgresql_10  (10.22, 10.21, 10.20, 10.19, 10.18, 10.17, 10.16, 10.15, 10.14, 10.13 ...)
* postgresql_11  (11.21, 11.20, 11.19, 11.18, 11.17, 11.16, 11.15, 11.14, 11.13, 11.12 ...)

Warning: Showing top 10 results and truncated versions. Use --show-all to show all

Det kan se slik ut! Da kan vi installere databasen ved hjelp av devbox add postgresql@13.

En database er en såkalt tjeneste som er ment å kjøre i bakgrunnen. Vi kan starte alle tjenestene prosjektet vårt er avhengig av ved å kjøre devbox services start.

En enklere hverdag

Se for deg at du har første arbeidsdag som utvikler. Du bruker git for å laste ned kildekoden til prosjektet du skal jobbe med, deretter kjører du devbox shell. Og det er det. Nå er det bare å skrive kode. Alt prosjekt-spesifikt er allerede installert for deg.

Hvis du ikke bruker en terminal-basert kode-editor finnes det en devbox plugin til VSCode, Zed og Eclipse. Devbox kan også generere en dockerfil for deg, og en devcontainer for de som bruker det.

Apropos, du kommer også til å ha en bruker-spesifik devbox.json fil. Denne kan du manipulere ved å bruke devbox global add for å legge til pakker, og devbox global rm for å fjerne pakker. Denne globale fila kan erstatte homebrew, for de som bruker det, og kan også sjekkes inn i Git hvis du vil ha en backup.

Til slutt finnes det også en github action for devbox, som gir deg et reproduserbart CI-miljø. Det betyr at konfigurasjonsfila til github actions ikke trenger å være stort mer enn en linje for å aktivere devbox pluginen etterfulgt av devbox run test.

Har devbox alle mulighetene til Nix? Nei. Men det har mer enn nok til å kunne bli et vanlig syn i utviklingsverdenen fremover.