nip/docs/static-build-guide.md

9.8 KiB

NIP Static Build Guide

Overview

This guide explains how to build NIP as a fully static binary for minimal deployment scenarios. Static builds are essential for the "minimal install philosophy" - enabling users to boot a tiny netinst image and build their perfect system from a single ~5MB binary.

Why Static Linking?

Benefits

  1. Zero Dependencies: Binary runs on any Linux system with kernel 4.19+
  2. Minimal Deployment: Single file, no package manager needed
  3. Predictable Behavior: No library version conflicts
  4. Portable: Works across different distributions
  5. Netinst Ready: Perfect for minimal installation images

Trade-offs

  • Larger Binary Size: ~5-10MB vs ~2-3MB dynamic
  • No Shared Libraries: Can't benefit from system library updates
  • Build Complexity: Requires careful configuration

Build Methods

The easiest way to build a static binary:

cd nip
./build_static.sh

This script:

  • Detects musl-gcc for optimal builds
  • Configures all static linking flags
  • Verifies the binary is truly static
  • Creates deployment package
  • Provides size comparison

Output:

  • nip-static - Fully static binary
  • nip-v0.2.0-weihnachtsmann-static-*.tar.gz - Deployment package

Method 2: Manual Build with Musl

For the smallest possible binary (recommended):

# Install musl-tools (Debian/Ubuntu)
sudo apt install musl-tools

# Or on Arch
sudo pacman -S musl

# Build with musl
nim c \
    --define:static \
    --define:release \
    --define:danger \
    --opt:speed \
    --mm:orc \
    --threads:on \
    --passC:-flto \
    --passL:-flto \
    --passL:-static \
    --passL:-s \
    --gcc.exe:musl-gcc \
    --gcc.linkerexe:musl-gcc \
    --out:nip-static \
    nip.nim

Expected Size: ~5-7MB

Method 3: Manual Build with Glibc

For maximum compatibility (larger binary):

nim c \
    --define:static \
    --define:release \
    --define:danger \
    --opt:speed \
    --mm:orc \
    --threads:on \
    --passC:-flto \
    --passL:-flto \
    --passL:-static \
    --passL:-static-libgcc \
    --passL:-s \
    --out:nip-static \
    nip.nim

Expected Size: ~8-12MB

Configuration Details

Static Linking Flags

The config.nims file includes static linking configuration when -d:static is defined:

when defined(static):
  # Core static linking
  switch("passL", "-static")
  switch("passL", "-static-libgcc")

  # Use musl if available
  when defined(linux):
    if fileExists("/usr/lib/x86_64-linux-musl/libc.a"):
      switch("gcc.exe", "musl-gcc")
      switch("gcc.linkerexe", "musl-gcc")

  # Optimization flags
  switch("passL", "-s")                    # Strip symbols
  switch("passC", "-flto")                 # Link-time optimization
  switch("passL", "-flto")
  switch("passC", "-ffunction-sections")   # Section garbage collection
  switch("passC", "-fdata-sections")
  switch("passL", "-Wl,--gc-sections")

Compiler Flags Explained

Flag Purpose Impact
--define:static Enable static build mode Activates static config
--define:release Release optimizations Faster code
--define:danger Disable runtime checks Smaller, faster
--opt:speed Optimize for speed Better performance
--mm:orc Use ORC memory manager Deterministic GC
--threads:on Enable threading Parallel operations
--passC:-flto Link-time optimization (C) Better code generation
--passL:-flto Link-time optimization (linker) Smaller binary
--passL:-static Static linking No dynamic deps
--passL:-s Strip symbols Smaller binary
--gcc.exe:musl-gcc Use musl compiler Smaller libc

Verification

Check Static Linking

# Should output: "not a dynamic executable"
ldd nip-static

# Or on some systems: "statically linked"
file nip-static

Check Binary Size

ls -lh nip-static
# Target: 5-10MB

Test Functionality

# Basic test (may require root)
./nip-static --version

# Full test
sudo ./nip-static setup
sudo ./nip-static status

Platform-Specific Notes

Linux (x86_64)

Recommended: Use musl-gcc for smallest binaries

# Debian/Ubuntu
sudo apt install musl-tools

# Arch
sudo pacman -S musl

# Fedora
sudo dnf install musl-gcc

Linux (ARM64)

# Install cross-compilation tools
sudo apt install gcc-aarch64-linux-gnu musl-tools

# Build for ARM64
nim c \
    --cpu:arm64 \
    --os:linux \
    --define:static \
    --gcc.exe:aarch64-linux-gnu-gcc \
    --gcc.linkerexe:aarch64-linux-gnu-gcc \
    --passL:-static \
    nip.nim

Linux (RISC-V)

# Install RISC-V toolchain
sudo apt install gcc-riscv64-linux-gnu

# Build for RISC-V
nim c \
    --cpu:riscv64 \
    --os:linux \
    --define:static \
    --gcc.exe:riscv64-linux-gnu-gcc \
    nip.nim

Troubleshooting

Problem: Binary has dynamic dependencies

Symptom:

ldd nip-static
# Shows: linux-vdso.so.1, libc.so.6, etc.

Solution:

  1. Ensure --passL:-static is used
  2. Check for dynamic library overrides
  3. Use musl-gcc instead of gcc
  4. Verify no -l flags without static variants

Problem: Binary is too large (>15MB)

Causes:

  • Not using musl (glibc is larger)
  • Debug symbols not stripped
  • LTO not enabled

Solutions:

# Strip manually if needed
strip nip-static

# Verify LTO is enabled
nim c --define:static --passC:-flto --passL:-flto ...

# Use musl
nim c --define:static --gcc.exe:musl-gcc ...

Problem: "undefined reference" errors

Symptom:

/usr/bin/ld: undefined reference to `pthread_create'

Solution: Add threading library explicitly:

nim c --define:static --passL:-lpthread ...

Problem: SSL/TLS not working

Symptom:

Error: SSL library not found

Solution: Ensure static SSL libraries are available:

# Debian/Ubuntu
sudo apt install libssl-dev

# Build with SSL
nim c --define:static --define:ssl --passL:-lssl --passL:-lcrypto ...

Size Optimization Tips

1. Use Musl Instead of Glibc

Savings: ~3-5MB

nim c --define:static --gcc.exe:musl-gcc nip.nim

Savings: ~1-2MB

nim c --define:static --passC:-flto --passL:-flto nip.nim

3. Strip Symbols

Savings: ~500KB-1MB

nim c --define:static --passL:-s nip.nim
# Or manually: strip nip-static

4. Enable Section Garbage Collection

Savings: ~500KB

nim c \
    --define:static \
    --passC:-ffunction-sections \
    --passC:-fda-sections \
    --passL:-Wl,--gc-sections \
    nip.nim

5. Use UPX Compression (Optional)

Savings: ~50-70% (but slower startup)

# Install UPX
sudo apt install upx-ucl

# Compress binary
upx --best --lzma nip-static

# Result: ~2-3MB compressed

Trade-off: Slower startup time (~100-200ms decompression)

Deployment

Minimal Netinst Image

Create a minimal bootable image with just NIP:

# 1. Create minimal rootfs
mkdir -p netinst/{bin,lib,etc,usr/local/bin}

# 2. Copy static binary
cp nip-static netinst/usr/local/bin/nip

# 3. Add minimal init
cat > netinst/init << 'EOF'
#!/bin/sh
exec /usr/local/bin/nip setup
EOF
chmod +x netinst/init

# 4. Create initramfs
cd netinst
find . | cpio -o -H newc | gzip > ../netinst.img

Result: ~50-100MB bootable image with NIP

Installation Script

The static build includes a minimal install script:

#!/bin/bash
# Ultra-minimal NIP installation
sudo cp nip /usr/local/bin/nip
sudo chmod +x /usr/local/bin/nip
sudo nip setup

CI/CD Integration

GitHub Actions Example

name: Build Static Binary

on: [push, pull_request]

jobs:
  build-static:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install Nim
        uses: jiro4989/setup-nim-action@v1
        with:
          nim-version: '2.0.0'

      - name: Install musl-tools
        run: sudo apt-get install -y musl-tools

      - name: Build static binary
        run: |
          cd nip
          ./build_static.sh          

      - name: Verify static linking
        run: |
          ldd nip/nip-static || echo "Fully static"          

      - name: Upload artifact
        uses: actions/upload-artifact@v3
        with:
          name: nip-static
          path: nip/nip-static

Performance Considerations

Startup Time

  • Dynamic binary: ~10-20ms
  • Static binary (uncompressed): ~15-30ms
  • Static binary (UPX compressed): ~100-200ms

Runtime Performance

Static binaries have identical runtime performance to dynamic binaries. The only difference is startup time.

Memory Usage

Static binaries may use slightly more memory (~1-2MB) because:

  • Entire libc is included
  • No shared library benefits

For NIP's use case (system package manager), this is negligible.

Best Practices

1. Always Verify Static Linking

ldd nip-static | grep -q "not a dynamic" || echo "WARNING: Not fully static"

2. Test on Minimal Systems

# Test in Docker with minimal base
docker run --rm -v $(pwd):/app alpine:latest /app/nip-static --version

3. Document Dependencies

Even static binaries have kernel requirements:

  • Linux kernel 4.19+ (for modern syscalls)
  • x86_64, ARM64, or RISC-V architecture

4. Provide Multiple Variants

Offer both static and dynamic builds:

  • Static: For minimal installs, netboot, embedded
  • Dynamic: For regular systems, faster startup

References


Document Version: 1.0 Last Updated: November 18, 2025 Applies To: NIP v0.2.0 "Weihnachtsmann"