kae3g 9999: Clojure & ClojureScript in the Nix Ecosystem
Timestamp: 12025-10-04–05thhouse01987
Series: Technical Writings (9999 → 0000)
Category: Package Management, Ecosystem Analysis
Reading Time: 20 minutes
The Question
What Clojure and ClojureScript packages exist in Nixpkgs, and how does this ecosystem integration shape our architectural decisions?
Introduction: Two Worlds Converging
{:clojure-ecosystem
{:heritage "Lisp tradition, JVM platform, Java interop"
:package-sources ["Maven Central" "Clojars"]
:build-tools ["Leiningen" "Boot" "Clojure CLI (tools.deps)"]
:philosophy "Dynamic, REPL-driven, simple over easy"}
:nix-ecosystem
{:heritage "Functional programming, purely functional builds"
:package-source "nixpkgs repository (~80,000 packages)"
:build-tool "Nix expression language"
:philosophy "Reproducible, declarative, composable"}}
When these two worlds meet, we find a fascinating intersection of philosophies—both rooted in functional programming, both valuing precision, yet approaching system-building from different angles.
Available Packages in Nixpkgs
Core Language & Runtimes
# Available in nixpkgs
{
clojure = pkgs.clojure; # Clojure CLI tools
leiningen = pkgs.leiningen; # Version 2.11.2 (as of research)
babashka = pkgs.babashka; # Fast-starting Clojure scripting
boot = pkgs.boot; # Alternative build tool
}
Clojure (CLI Tools)
{:name "clojure"
:description "The core Clojure programming language and CLI tools"
:provides ["clj" "clojure" "deps.edn support"]
:jvm-requirement "JDK 8 or higher"
:usage
"nix-shell -p clojure --run 'clj'"}
Philosophy Connection:
"The Tao that can be told is not the eternal Tao."
— Laozi
Clojure embodies this: a language that values the untold, the implicit, the power of doing less. Nix packages it, making the ineffable reproducible.
Babashka
{:name "babashka"
:description "Native, fast-starting Clojure interpreter"
:implementation "GraalVM native-image"
:startup-time "< 50ms"
:why-it-matters
"Breaks the JVM startup barrier—Clojure becomes scriptable"
:nix-advantage
"Babashka's native binary plays beautifully with Nix's
deterministic builds—no JVM classpath complexity"}
Aristotelian Analysis:
Babashka represents the actuality of Clojure without the potentiality burden of the JVM. In Aristotelian terms, it's Clojure achieving its telos (purpose) for the scripting domain.
Development Tools
{
clj-kondo = pkgs.clj-kondo; # Linter for Clojure/ClojureScript
clojure-lsp = pkgs.clojure-lsp; # LSP implementation
jet = pkgs.jet; # JSON/EDN/Transit converter
}
clj-kondo: The Linter That Sparks Joy
{:name "clj-kondo"
:tagline "A linter for Clojure code that sparks joy"
:implementation "Native binary (GraalVM)"
:capabilities
["Syntax checking"
"Unused namespace detection"
"Arity checking"
"Type-based warnings"
"Custom lint rules"]
:integration
["Emacs" "Vim" "VS Code" "IntelliJ" "CI/CD"]
:nix-benefit
"Install once, works everywhere—no plugin hell"}
Hebrew Wisdom Parallel:
"The fear of the Lord is the beginning of wisdom."
— Proverbs 9:10
Replace "fear" with "respect for the code," and clj-kondo becomes the tool that instills this reverence—catching errors before they become production issues.
clojure-lsp: Language Server Protocol
{:name "clojure-lsp"
:protocol "LSP (Language Server Protocol)"
:features
["Auto-completion"
"Go to definition"
"Find references"
"Refactoring support"
"Documentation on hover"]
:architecture
"Server-client—one LSP server, many editor clients"
:nix-integration
"nix-shell -p clojure-lsp
# Now available to all LSP-compatible editors"}
Community Integration Tools
clj-nix: The Bridge Builder
# Not in nixpkgs directly, but critical for ecosystem integration
{
inputs.clj-nix.url = "github:jlesquembre/clj-nix";
# Provides functions:
# - mkCljCli: Build Clojure CLI projects
# - bbTasksFromFile: Wrap Babashka tasks as Nix derivations
# - mk-deps-cache: Generate reproducible dependency caches
}
{:problem
"Leiningen/tools.deps want to download deps at runtime.
Nix requires all dependencies at build time.
This is a fundamental impedance mismatch."
:solution-via-clj-nix
"Pre-download all Maven/Clojars dependencies,
create a fixed dependency cache,
pass it to the Clojure build tool."
:result
"Reproducible Clojure builds that satisfy Nix's purity requirements"}
The Impedance Mismatch: Clojure ↔ Nix
The Fundamental Tension
{:clojure-model
{:dependency-resolution :runtime
:classpath :dynamic
:repl :essential
:iteration-speed :critical
:example
"Change deps.edn → Run 'clj' → Downloads new deps → REPL ready"}
:nix-model
{:dependency-resolution :build-time
:store-paths :immutable
:purity :essential
:reproducibility :critical
:example
"Change flake.nix → Run 'nix build' → All deps pre-fetched → Binary ready"}}
Why This Matters: The Parable of the Two Wells
The Clojure Well:
Water flows freely. You come with a bucket whenever you need, and the well provides. Fast, responsive, always available.
The Nix Well:
Water is drawn once, stored in sealed jars, labeled and cataloged. When you need water, you take from your jars. The well is consulted only when you need a new jar filled.
Both are valid. Both serve different masters:
- Clojure serves iteration speed and REPL-driven development
- Nix serves reproducibility and deployment certainty
Clojure Eager Loading vs Nix Lazy Loading: The Package Build System Philosophy
The Fundamental Loading Difference
{:clojure-eager-loading
{:philosophy "Load everything immediately"
:behavior "All namespaces loaded at startup"
:memory-usage "Higher initial consumption"
:startup-time "Slower initial load"
:runtime-performance "Faster subsequent operations"
:example
"(require '[clojure.string :as str])
(require '[clojure.data.json :as json])
;; Both loaded immediately, available instantly"
:network-implications
"Perfect for networked language environments where:
- Dependencies are fetched once
- All functionality needed immediately
- JVM warmup amortizes loading cost"
:package-build-system-fit
"Ideal for package build systems because:
- All tools available without lazy loading delays
- Consistent behavior across different network conditions
- Predictable memory usage for CI/CD environments"}
:nix-lazy-loading
{:philosophy "Load only what's needed, when needed"
:behavior "Derivations built on-demand"
:memory-usage "Lower initial consumption"
:startup-time "Faster initial access"
:runtime-performance "Slower when accessing new derivations"
:example
"nix-shell -p clojure # Only loads Clojure
nix-shell -p babashka # Only loads Babashka
# Each derivation is independent, lazy-loaded"
:network-implications
"Perfect for system package management where:
- Packages installed incrementally
- Disk space optimization matters
- Different users need different tool sets"
:package-build-system-fit
"Challenging for package build systems because:
- Build tools need all dependencies available immediately
- Lazy loading can cause unexpected delays
- CI/CD needs predictable, fast builds"}}
Why Clojure's Eager Loading Wins for Networked Languages
{:the-network-reality
"In a networked language environment:
1. Dependencies are fetched over network (Maven Central, Clojars)
2. Network latency is unpredictable
3. Build systems need deterministic behavior
4. All tools must be available immediately
Clojure's eager loading philosophy aligns perfectly:
- Pay the network cost once
- Load everything into memory
- Fast subsequent operations
- Predictable behavior"
:nix-lazy-loading-challenges
"Nix's lazy loading creates friction in networked environments:
- Each tool loaded separately
- Potential for network delays during lazy loading
- Unpredictable timing in CI/CD pipelines
- Complex dependency graphs can cause cascading delays
This is why clj-nix exists—to bridge this philosophical gap."}
🔗 Connection to infuse.nix (from 9998)
The tension between eager and lazy loading parallels the infuse.nix paradigm (detailed in 9998):
{:infuse-as-eager-loading-for-configs
"Traditional Nix: Lazy evaluation (load modules on-demand)
- Evaluation happens when needed
- Can cause unexpected delays
- Complex dependency resolution
infuse.nix: Eager resolution (build-time dependency closure)
- All dependencies resolved at build time
- Predictable timing (like Clojure's eager loading)
- Service 'infusions' are complete closures
This mirrors Clojure's eager loading philosophy:
Pay the cost once (build time), then fast operations (deployment).
See 9998 for hands-on infuse.nix examples and learning path."}
Educational Background: System Calls and the Unix Model
The Unix Philosophy: "Everything is a File"
{:unix-model-foundation
{:core-principle "Everything is a file"
:system-calls
["open()" "read()" "write()" "close()"
"fork()" "exec()" "wait()"
"pipe()" "socket()" "bind()" "listen()"]
:file-descriptors
"Every open file, socket, pipe gets a file descriptor
- stdin (0), stdout (1), stderr (2)
- Network sockets get higher numbers
- Process can have up to 1024 file descriptors (typically)"
:process-model
"fork() creates child process (copy of parent)
exec() replaces process image with new program
wait() waits for child process to complete"
:inter-process-communication
"pipes, sockets, shared memory, signals
All mediated through kernel system calls"}}
How System Calls Work Under the Hood
{:system-call-mechanism
{:user-space-to-kernel
"1. User program calls library function (e.g., write())
2. Library function triggers system call interrupt
3. CPU switches to kernel mode
4. Kernel validates parameters and executes
5. Kernel returns result to user space
6. CPU switches back to user mode"
:kernel-responsibilities
["Memory management (virtual memory, page tables)"
"Process scheduling and context switching"
"File system operations"
"Network stack management"
"Device driver coordination"
"Security enforcement (permissions, capabilities)"]
:performance-implications
"System calls are expensive:
- Context switch from user to kernel mode
- Parameter validation
- Kernel processing
- Context switch back to user mode
This is why buffering and batching matter."}}
Educational Background: Buffering and Batching
The System Call Performance Problem
{:the-fundamental-issue
"Every system call involves:
1. User space → kernel space transition
2. Parameter validation
3. Kernel processing
4. Kernel space → user space transition
Each transition costs ~1000-10000 CPU cycles
In a 3GHz CPU: ~0.3-3 microseconds per transition
For 1000 small writes: 1000 × 6 microseconds = 6ms overhead
Just for context switching, not actual work!"}
{:real-world-example
"Writing 1MB file with 1-byte writes:
Without buffering:
- 1,048,576 system calls
- ~6-60 seconds just for system call overhead
- Actual disk I/O: ~1ms
With 4KB buffer:
- 256 system calls
- ~1.5ms system call overhead
- Actual disk I/O: ~1ms
- Total: ~2.5ms vs 6-60 seconds"}
Buffering: The Art of Accumulation
{:buffering-philosophy
"Instead of immediate system calls, accumulate data in memory
then flush when buffer is full or explicitly requested"
:buffer-types
{:user-space-buffers
"Application-managed buffers (malloc'd memory)
- stdio buffering (FILE* streams)
- Application-level buffering
- Custom buffer implementations"
:kernel-buffers
"Operating system managed buffers
- Page cache (file system buffers)
- Network socket buffers
- Device driver buffers"
:hardware-buffers
"Device-level buffering
- Disk controller cache
- Network card buffers
- CPU cache hierarchy"}
:buffering-strategies
{:full-buffering
"Buffer accumulates until full, then flush
- Good for: Large, sequential writes
- Bad for: Interactive applications
- Example: Writing large files"
:line-buffering
"Flush when newline character encountered
- Good for: Terminal output, logs
- Bad for: Binary data
- Example: printf() to stdout"
:unbuffered
"Immediate flush after every operation
- Good for: Critical data, error logging
- Bad for: Performance
- Example: stderr, critical logs"}}
Batching: The Art of Grouping
{:batching-philosophy
"Instead of individual operations, group multiple operations
into single system calls or kernel operations"
:batching-examples
{:readv-writev
"Scatter-gather I/O operations
- Single system call for multiple buffers
- Reduces system call overhead
- Example: writev() for multiple data chunks"
:batch-system-calls
"Group related system calls
- Single kernel entry for multiple operations
- Atomic operations
- Example: futex() for thread synchronization"
:network-batching
"TCP_NODELAY vs TCP_CORK
- TCP_NODELAY: Send immediately (interactive)
- TCP_CORK: Batch small packets (throughput)
- Example: Web servers batching HTTP responses"}
:batching-benefits
{:reduced-overhead
"Fewer system calls = less context switching
- 100 operations → 1 system call
- 99% reduction in system call overhead"
:improved-cache-locality
"Related operations stay together
- Better CPU cache utilization
- Reduced memory access patterns"
:atomic-operations
"Multiple operations as single unit
- Consistency guarantees
- Reduced race conditions"}}
The Clojure-Nix Connection: Buffering and Batching Strategies
{:clojure-buffering-advantages
{:jvm-memory-management
"JVM provides sophisticated buffering:
- Object pooling
- Garbage collection optimization
- Memory-mapped files
- NIO (Non-blocking I/O) buffers"
:clojure-specific-buffering
"Clojure's persistent data structures:
- Structural sharing reduces copying
- Lazy sequences for memory efficiency
- Transients for batch updates
- Chunked sequences (32 elements per chunk)"
:network-buffering
"Clojure's eager loading philosophy:
- Load all dependencies into memory at startup
- No lazy loading delays during execution
- Predictable memory usage patterns
- Perfect for buffered operations"}
:nix-lazy-loading-challenges
{:on-demand-access
"Each package accessed individually:
- More system calls for package resolution
- Potential for network delays
- Unpredictable timing patterns
- Cache misses for related packages"
:derivation-granularity
"Each derivation is independent:
- No batching of related packages
- Individual build processes
- Separate dependency resolution
- Higher system call overhead"}
:the-hybrid-solution
"Best of both worlds:
- Use Nix for system-level package management (buffered installation)
- Use Clojure's eager loading for application dependencies (batched loading)
- Let each tool optimize for its domain"}}
Real-World Performance Implications
{:performance-comparison
{:clojure-eager-loading
"Startup cost: High (load everything)
Runtime cost: Low (everything in memory)
Memory usage: Predictable and stable
Network calls: Few (fetch once at startup)
System calls: Fewer (buffered operations)"
:nix-lazy-loading
"Startup cost: Low (load minimal set)
Runtime cost: Variable (on-demand loading)
Memory usage: Variable and unpredictable
Network calls: Many (fetch on demand)
System calls: More (individual package access)"}
:when-to-use-which
{:use-clojure-eager-loading-when
["Building package build systems"
"CI/CD pipelines need predictable timing"
"All dependencies needed immediately"
"Network latency is high or unpredictable"
"Memory usage can be high for performance"]
:use-nix-lazy-loading-when
["System package management"
"Different users need different tool sets"
"Disk space optimization matters"
"Memory usage must be minimal"
"Packages installed incrementally"]}}
Alternative Init Systems: Beyond systemd
The Init System Landscape
{:init-system-philosophy
"The init system is PID 1—the first userspace process
Everything else descends from it
Choice of init system affects:
- Boot speed
- Service management
- System complexity
- Resource usage
- Dependency resolution"
:the-systemd-paradigm
{:philosophy "Monolithic, feature-rich, all-in-one"
:components
["init system" "service manager" "device manager (udev)"
"login manager" "network manager" "DNS resolver"
"time synchronization" "logging" "container management"]
:advantages
["Powerful service management"
"Parallel service startup"
"Unified logging (journald)"
"Socket activation"
"Extensive documentation"]
:criticisms
["Monolithic design violates Unix philosophy"
"Large binary (~1.5MB+)"
"Complex dependencies"
"Mission creep (feature bloat)"
"Opaque binary logs"]}
:the-alternative-philosophy
"Small, focused tools that do one thing well
Composable components
Text-based configuration
Minimal resource usage
Clear separation of concerns"}
SixOS: NixOS Without systemd
{:sixos-overview
{:what-is-it
"A NixOS variant that replaces systemd with s6 supervision suite
Created by Laurent Bercot (skarnet software)
Maintains NixOS benefits while embracing Unix philosophy"
:s6-supervision-suite
"A complete init system and service manager
- s6: service supervision
- s6-rc: service manager
- s6-linux-init: init system
All written in C, highly portable, minimal footprint"
:key-innovation-infuse-combinator
"Services managed like packages in nixpkgs
- Declarative service definitions
- Atomic activation
- Rollback capabilities
- Same immutability guarantees as NixOS"}
:sixos-advantages
{:maintains-nix-benefits
["Atomic configuration activation"
"Immutable system configurations"
"Reproducible builds"
"Generation rollback"]
:adds-unix-philosophy
["Small, focused components"
"Text-based service definitions"
"No binary logging"
"Minimal resource usage"
"Clear component boundaries"]
:hardware-support
"Ownerboot: Manages mutable firmware as part of system config
All firmware tracked and versioned like packages"}
:comparison-to-nixos
{:nixos-systemd
"Full systemd integration
~1.5MB systemd binary
Binary journald logs
Complex service dependencies"
:sixos-s6
"s6 supervision suite
~200KB total binaries
Text-based logs
Simple dependency chains"}}
Practical Example: infuse.nix in Action
Here's a concrete example of how SixOS uses infuse.nix (from 9998) to configure services:
# Traditional NixOS (verbose, nested):
services.postgresql = {
enable = true;
package = pkgs.postgresql_15;
settings = {
shared_buffers = "256MB";
max_connections = 200;
};
};
# SixOS with infuse.nix (clear, path-based):
infuse services.postgresql {
enable.__assign = true;
package.__assign = pkgs.postgresql_15;
settings.shared_buffers.__assign = "256MB";
settings.max_connections.__assign = 200;
}
# Or create multiple instances (impossible in traditional NixOS):
services.postgresql-prod = infuse basePostgres {
env.PGPORT.__assign = "5432";
env.POSTGRES_DB.__assign = "production";
};
services.postgresql-test = infuse basePostgres {
env.PGPORT.__assign = "5433";
env.POSTGRES_DB.__assign = "testing";
};
# Result: Two PostgreSQL instances, different configs, same base
# This is the power of treating services like packages
Why this matters:
- Each
infusecreates a Nix derivation → s6-rc service bundle - Build-time resolution (eager) → predictable deployment
- Path-based syntax → clear intent
- Multiple instances → production + staging + testing on same machine
See 9953: The infuse.nix Paradigm for full tutorial.
OpenRC: Dependency-Based Init
{:openrc-overview
{:philosophy "Flexible, dependency-based, Unix-friendly"
:heritage "Gentoo Linux, Alpine Linux"
:design "Service scripts + dependency management"
:key-features
["Dependency-based service startup"
"Parallel service initialization"
"Service supervision support"
"Compatible with multiple init systems"
"Shell-based service scripts"]
:service-definition-example
"#!/sbin/openrc-run
name=\"my-service\"
command=\"/usr/bin/my-daemon\"
command_args=\"--config /etc/my-service.conf\"
depend() {
need net
use dns
after firewall
}"}
:openrc-strengths
{:flexibility
"Can work with multiple supervision systems:
- Built-in process supervision
- Integration with runit
- Integration with s6
- Standalone operation"
:efficiency
"Parallel service startup based on dependencies
Minimal overhead
Fast boot times
Clear dependency resolution"
:compatibility
"Gentoo, Alpine, Artix, Devuan
Works on Linux and BSD systems
Shell-based configuration (familiar to Unix admins)"}}
Runit: Simple Service Supervision
{:runit-overview
{:philosophy "Do one thing well: supervise services"
:design "Three-stage init, simple supervision"
:heritage "daemontools lineage, inspired by djb's work"
:three-stage-boot
"Stage 1: System initialization (one-time setup)
Stage 2: Start supervised services
Stage 3: System shutdown"
:service-supervision
"Each service gets a directory with:
- run script (starts the service)
- finish script (cleanup after service exits)
- Optional log/run (dedicated logging)"}
:runit-service-example
"# /etc/sv/my-service/run
#!/bin/sh
exec chpst -u myuser /usr/bin/my-daemon 2>&1
# /etc/sv/my-service/log/run
#!/bin/sh
exec svlogd -tt /var/log/my-service"
:runit-advantages
{:simplicity
"~10KB per service (run + finish scripts)
No complex configuration
Plain shell scripts
Easy to understand and debug"
:reliability
"Automatic service restart on failure
Per-service logging
No zombies (process reaping)
Clear service state"
:performance
"Minimal resource usage
Fast service startup
Low memory footprint
No unnecessary features"}}
Rust-Based Init Systems: Modern Alternatives
{:rust-init-systems
{:why-rust-for-init
"Memory safety without garbage collection
Zero-cost abstractions
Fearless concurrency
No runtime overhead
Modern tooling and ecosystem"
:rinit
{:description
"Next-generation init and service manager in Rust
Inspired by s6 and daemontools
Work in progress, use with caution"
:features
["Different program types (oneshot, longrun, bundle)"
"Predictable dependencies at build time"
"Asynchronous service start"
"Low resource footprint"
"Safe parallelism"]
:design-goals
"Combine s6's simplicity with Rust's safety
Modern service management
Suitable for desktop and server"}
:nitro
{:description
"Minimalist init system and process supervisor in Rust
Designed for embedded systems and appliances"
:features
["Platform-specific system calls"
"Basic filesystem setup"
"Simple service supervision"
"Single-application focus"
"Minimal binary size"]
:use-cases
"Embedded Linux systems
Container init (PID 1 in containers)
Single-purpose appliances
Minimal resource environments"}
:rust-supervision-libraries
{:daemonize-rs "Rust library for creating daemons"
:supervisor "Rust process supervisor library"
:tokio-process "Async process management with Tokio"
:nix-crate "Rust bindings for Unix system calls"}}}
OpenRC + Runit Integration: Best of Both Worlds
{:openrc-runit-integration
{:philosophy
"OpenRC for dependency management and orchestration
Runit for service supervision and reliability"
:how-it-works
"1. OpenRC determines service start order (dependencies)
2. OpenRC launches services via runit supervision
3. Runit monitors and restarts services if they fail
4. OpenRC tracks overall service state"
:example-integration
"# /etc/init.d/my-service (OpenRC script)
#!/sbin/openrc-run
supervisor=runit
command=\"/usr/bin/my-daemon\"
depend() {
need net
use dns
}
# OpenRC will create runit service directory
# Runit will supervise the actual process"}
:advantages-of-integration
{:from-openrc
["Dependency-based startup order"
"Service state tracking"
"Familiar service management commands"
"Cross-distribution compatibility"]
:from-runit
["Automatic service restart"
"Per-service logging"
"Reliable supervision"
"No zombie processes"]
:combined-benefits
"Dependency resolution + supervision
Parallel startup + reliability
Shell-based configuration + process monitoring"}}
Implications for Nix-Based Package Build Systems
{:init-system-for-package-builds
{:systemd-with-nixos
"Advantages:
- Deep integration with NixOS
- Service activation via nix-daemon
- Socket activation for builds
- Well-tested in Nix ecosystem
Disadvantages:
- Heavy resource usage
- Complex dependency graph
- Binary configuration
- Feature bloat for build environments"
:s6-with-sixos
"Advantages:
- Minimal resource usage
- Text-based configuration
- Fast service startup
- Unix philosophy alignment
- Perfect for containers and CI/CD
Disadvantages:
- Less mature than systemd integration
- Smaller community
- Fewer ready-made service definitions"
:openrc-runit-combination
"Advantages:
- Dependency management (OpenRC)
- Reliable supervision (runit)
- Lightweight and fast
- Well-suited for Alpine Linux + Nix
Disadvantages:
- Manual integration with Nix
- Not native to NixOS
- Requires custom configuration"}
:recommendations-for-package-build-systems
{:ci-cd-containers
"Use SixOS or Alpine + OpenRC/runit
- Minimal resource usage
- Fast boot times
- Predictable behavior
- Easy to reproduce"
:development-workstations
"Use NixOS with systemd
- Full feature set
- Desktop environment support
- Hardware management
- User services"
:production-servers
"Consider SixOS or systemd-free NixOS variant
- Reduced attack surface
- Lower resource usage
- Simpler debugging
- Text-based logs"}
:the-ideal-hybrid
"For package build systems networked across different environments:
- Use Nix for package management (universal)
- Choose init system based on deployment target:
* Containers/CI: s6 or runit (minimal)
* Workstations: systemd (full-featured)
* Servers: OpenRC + runit (balanced)
- Keep service definitions in Nix expressions
- Generate init-specific configurations from Nix
- Maintain reproducibility across all init systems"}}
📚 Learning Path Connection
Want to master these init systems hands-on?
See 9998: Learning Path for Init Systems for:
- Phase 1: SixOS deep dive with infuse.nix paradigm
- Phase 2: OpenRC & runit practical mastery
- Phase 3: Build your own Rust supervisor
- Phase 4: Integration strategies and production deployment
Ready to implement on real hardware? See 9997: Framework Laptop Setup for:
- Choosing between Void Linux (musl + xbps-src) and Artix (Arch + runit)
- Cosmopolitan libc for portable binaries
- Complete installation and workflow guides
Alpine Linux: Minimalist Unix Implementation
{:alpine-linux-model
{:philosophy "Minimal, secure, simple"
:base-system
"musl libc (instead of glibc)
busybox (minimal Unix utilities)
OpenRC (init system)
~5MB base image"
:security-focus
"Position-independent executables (PIE)
Stack smashing protection
Minimal attack surface
Regular security updates"
:container-optimization
"Designed for containers from the ground up
Fast boot times
Minimal resource usage
Perfect for microservices and CI/CD"
:system-call-efficiency
"musl libc optimized for size and speed
Fewer system call overhead
Direct kernel interface
Minimal abstraction layers"}}
Darwin/macOS Sonoma: Complex Unix Adaptation
{:darwin-macos-complexity
{:foundation "BSD Unix + Mach microkernel"
:layered-architecture
"Mach kernel (microkernel)
BSD layer (Unix compatibility)
I/O Kit (device drivers)
Core Services (foundation frameworks)
Application Services (GUI, networking)
Application layer (user programs)"
:system-call-evolution
"Traditional BSD system calls
Mach messages for microkernel communication
Hybrid approach for performance
Complex IPC between layers"
:modern-additions
["Grand Central Dispatch (concurrency)"
"Core Foundation (object-oriented C)"
"Cocoa/Objective-C runtime"
"Swift runtime and ARC"
"Metal (GPU programming)"
"CloudKit integration"]
:performance-implications
"More abstraction layers = more overhead
But better security and functionality
JIT compilation and optimization
Hardware-specific optimizations"}
:vs-alpine-comparison
{:alpine
"Simple, fast, minimal
Direct system calls
~5MB base
Perfect for containers"}
{:macos
"Complex, feature-rich, heavy
Multiple abstraction layers
~10GB+ base system
Perfect for desktop/laptop development"}}}
The Networked Language Connection
{:why-this-matters-for-package-systems
{:clojure-eager-loading-advantage
"In networked environments:
- All dependencies loaded once at startup
- No lazy loading delays during execution
- Predictable memory and timing
- Perfect for CI/CD pipelines"
:nix-lazy-loading-challenge
"In networked environments:
- Each package loaded on-demand
- Network delays during lazy loading
- Unpredictable timing
- Complex dependency resolution"
:system-call-implications
"Clojure's eager loading minimizes system calls:
- Load all files once
- Keep in memory
- Fewer open/read/close cycles
Nix's lazy loading maximizes system calls:
- Load packages on-demand
- More file system access
- More network calls for missing packages"
:the-ideal-hybrid
"Best of both worlds:
- Use Nix for system-level package management
- Use Clojure's eager loading for application dependencies
- Let each tool do what it does best"}}
Bridging Strategies
Strategy 1: Babashka + Nix (Harmonious)
# flake.nix
{
outputs = { self, nixpkgs }: {
devShells.x86_64-linux.default =
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in pkgs.mkShell {
buildInputs = [ pkgs.babashka ];
shellHook = ''
echo "Babashka + Nix: Fast iteration, reproducible environment"
bb --version
'';
};
};
}
Why This Works:
{:babashka-nix-harmony
"Babashka scripts are typically self-contained or use built-in libraries.
Less dependency on external Maven artifacts.
Fast startup matches Nix's philosophy of deterministic execution.
Result: Beautiful marriage of two functional approaches."}
Strategy 2: Clojure CLI + clj-nix (Requires Ceremony)
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
clj-nix.url = "github:jlesquembre/clj-nix";
};
outputs = { self, nixpkgs, clj-nix }: {
packages.x86_64-linux.my-clojure-app =
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
cljpkgs = clj-nix.packages.x86_64-linux;
in cljpkgs.mkCljBin {
projectSrc = ./.;
name = "my-app";
main-ns = "my-app.core";
# clj-nix handles Maven dependency resolution
jdkRunner = pkgs.jdk17;
};
};
}
Strategy 3: Leiningen + Nix (Manual Labor)
# The hard way: manually manage Maven dependencies
{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
name = "my-clojure-app";
src = ./.;
buildInputs = [ pkgs.leiningen pkgs.jdk17 ];
# Pre-download Maven deps to avoid network access during build
LEIN_HOME = ./lein-cache; # Populated separately
buildPhase = ''
lein uberjar
'';
installPhase = ''
mkdir -p $out/bin
cp target/my-app-standalone.jar $out/
# ... create wrapper script ...
'';
}
The Challenge:
{:leiningen-nix-friction
"Leiningen expects to download deps from project.clj.
Nix forbids network access during builds.
Workarounds:
1. Pre-populate LEIN_HOME with all deps (tedious)
2. Use Nix to create a 'maven-repository.nix' derivation (complex)
3. Use clj-nix or similar tooling (best option)"
:philosophical-question
"Should we bend Leiningen to fit Nix,
or use tools designed with Nix in mind (Babashka, Clojure CLI)?"}
Ecosystem Completeness Analysis
What's Available ✓
{:core-tools
["clojure" "leiningen" "babashka" "boot"]
:development-support
["clj-kondo" "clojure-lsp" "jet"]
:jvm-runtime
["Multiple JDK versions (8, 11, 17, 21)"]
:build-integration
["clj-nix (community)" "Custom Nix expressions"]}
What's Missing ✗
{:missing-from-nixpkgs
["shadow-cljs" "Native ClojureScript build tool"]
["figwheel" "ClojureScript hot reloading"]
["calva" "VS Code Clojure extension (not applicable to Nix)"]
:workarounds
"Use npm/yarn/pnpm to install shadow-cljs and Figwheel.
Nix can manage Node.js and npm, then delegate to npm for these tools."}
The Long Tail: Library Dependencies
{:the-reality
"Most Clojure libraries aren't in nixpkgs.
They live on Maven Central and Clojars.
This is by design:
- Clojure has ~10,000+ libraries
- Nixpkgs can't package them all
- Maven/Clojars already provide distribution
Nix's role:
- Provide language runtimes (JDK, Clojure CLI, Babashka)
- Provide build tools (Leiningen, clj-nix)
- Provide development tools (clj-kondo, clojure-lsp)
- Let deps.edn/project.clj handle library deps"
:analogous-to
"Nix provides Python interpreter, pip, virtualenv.
It doesn't package every PyPI library.
Same philosophy applies to Clojure."}
Architectural Implications for Our Project
Given our project's current stack:
{:our-stack
{:language "Clojure (JVM for bb.edn tasks)"
:scripting "Babashka"
:frontend "ClojureScript → Svelte (generated)"
:package-manager "Nix (flake.nix)"
:build-orchestration "Babashka tasks"
:current-flake-nix
["babashka" "clojure" "nodejs_22"]}
:recommendation-add
["clj-kondo" "For linting Clojure code"
"clojure-lsp" "For editor integration"
"jet" "For EDN/JSON transformations if needed"]}
Recommended flake.nix Enhancement
{
description = "Robotic Farm: Sacred documentation through transformation";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.${system}.default = pkgs.mkShell {
buildInputs = with pkgs; [
# Core Clojure tooling
babashka
clojure
# Development tools
clj-kondo # NEW: Linting
clojure-lsp # NEW: LSP support
# Frontend tooling
nodejs_22
# Utilities
jet # NEW: EDN/JSON conversion
];
shellHook = ''
echo "╔════════════════════════════════════════╗"
echo "║ Robotic Farm Development Shell ║"
echo "║ From generation to generation ║"
echo "╚════════════════════════════════════════╝"
echo ""
echo "Available tools:"
echo " bb $(bb --version)"
echo " clj $(clj --version 2>&1 | head -n1)"
echo " clj-kondo $(clj-kondo --version)"
echo " node v$(node --version | cut -c2-)"
echo ""
echo "Run 'bb tasks' to see available commands"
'';
};
# Future: Add Docker image builds here
# packages.${system}.docker = ...
};
}
The Confucian Principle: Five Relationships
Just as Confucius taught five fundamental relationships (五倫), our Clojure-Nix integration involves five key relationships:
{:five-relationships
{1 {:entities ["Developer" "Development Environment"]
:mediated-by "nix develop"
:virtue "Consistency (一致性)"
:teaching "The environment should not change beneath the developer's feet"}
2 {:entities ["Source Code" "Dependencies"]
:mediated-by "deps.edn or project.clj"
:virtue "Clarity (明確性)"
:teaching "Dependencies should be explicit, not hidden"}
3 {:entities ["Build Process" "Artifacts"]
:mediated-by "Nix derivations"
:virtue "Reproducibility (可重現性)"
:teaching "Same input → same output, always"}
4 {:entities ["Language Ecosystem" "System Ecosystem"]
:mediated-by "clj-nix bridge tools"
:virtue "Harmony (和諧)"
:teaching "Two worlds need not clash—find the middle way"}
5 {:entities ["Development" "Deployment"]
:mediated-by "flake.lock and Docker images"
:virtue "Continuity (連續性)"
:teaching "Dev and prod should be identical, not similar"}}}
Practical Recommendations
For New Projects
{:starting-from-scratch
{:language-choice
"Prefer Babashka for CLI tools and scripts.
Use Clojure CLI (tools.deps) for larger applications.
Avoid Leiningen unless you need specific plugins."
:rationale
"Babashka integrates seamlessly with Nix.
Clojure CLI is simpler and more Nix-friendly than Leiningen.
Less ceremony = less friction."
:nix-shell-structure
"Use flake.nix for all tooling.
Let deps.edn handle Clojure library dependencies.
Two-layer approach: Nix for tools, deps.edn for libs."}
:example-project-structure
".
├── flake.nix # Nix: bb, clj, clj-kondo, node
├── flake.lock # Pinned Nix dependencies
├── deps.edn # Clojure library dependencies
├── bb.edn # Babashka tasks
└── src/
└── my_project/
└── core.clj"}
For Existing Projects
{:migrating-to-nix
{:step-1 "Add flake.nix for development environment only"
:step-2 "Install tools (bb, clj, clj-kondo) via Nix"
:step-3 "Keep existing build process (lein, boot, clj) unchanged initially"
:step-4 "Gradually introduce Nix-based builds using clj-nix if needed"
:step-5 "Optionally: Generate Docker images with Nix"
:philosophy
"Incremental adoption—don't rewrite everything.
Nix provides immediate value even just for dev environment consistency."}
:red-flags
["Don't try to package every Clojure library in Nix"
"Don't fight Leiningen's runtime dependency resolution"
"Don't convert existing projects to Babashka just for Nix"]}
Conclusion: The Middle Way
{:synthesis
"Clojure and Nix are both functional, both principled.
The tension between them is generative, not destructive.
Clojure optimizes for:
- Developer velocity
- REPL-driven development
- Java interop
Nix optimizes for:
- Deployment confidence
- Reproducible environments
- System-wide consistency
Use both:
- Nix for the outer shell (tools, runtimes, system deps)
- Clojure tools for the inner build (library deps, compilation)
Where they meet:
- Babashka (naturally harmonious)
- clj-nix (purposefully bridging)
- Docker images (Nix builds, Clojure runs)"
:from-the-analects
"The Master said: 'The firm, the enduring, the simple, and the modest
are near to virtue.'
— Confucius, Analects 13.27
Apply this:
- Firm: Reproducible builds (Nix)
- Enduring: Long-term maintainability (both)
- Simple: Minimal tooling (Babashka)
- Modest: Don't over-engineer (pragmatic integration)"}
References & Further Reading
{:official-docs
["https://nixos.org/manual/nixpkgs/stable/#sec-language-clojure"
"https://babashka.org/"
"https://clojure.org/guides/deps_and_cli"
"https://github.com/jlesquembre/clj-nix"]
:community-resources
["NixOS Discourse: Clojure builds"
"r/Clojure: Nix integration discussions"
"Clojurians Slack: #nix channel"]
:philosophical-sources
["Laozi: Tao Te Ching (Chapter 48 on simplicity)"
"Confucius: Analects (on consistency and relationships)"
"Aristotle: Nicomachean Ethics (on potentiality and actuality)"]}
Next Writing: 9998-learning-path-init-systems.md — A Comprehensive Learning Path for Modern Init Systems
Previous Writing: (none — this is 9999, the beginning)
"The journey of a thousand miles begins with one step."
— Laozi, Tao Te Ching, Chapter 64
But we count backward, from vision to implementation.
Copyright © 2025 kae3g | Dual-licensed under Apache-2.0 / MIT
Competitive technology in service of clarity and beauty