diff options
| author | Ben Sima <ben@bsima.me> | 2025-11-15 10:06:18 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bsima.me> | 2025-11-15 10:06:18 -0500 |
| commit | b3dd5f285365f59153a8f4549efa0607ccddf19d (patch) | |
| tree | 60f4fbe7a8d4e858f069f997546c2b3337a0002a | |
| parent | 905c6aa06c00150f0c051ead776a64aee0b2212c (diff) | |
Fix NixOS integration: separate package building from OS builds
Problem: Calling bild.run inside NixOS configs triggered IFD during
OS evaluation. ANSI escape codes from bild broke JSON parsing in Nix
sandbox, causing build failures.
Root cause: bild.run uses IFD (Import From Derivation) which runs
bild --plan during Nix evaluation. When this happened inside NixOS
service definitions, it ran recursively and bild output ANSI codes
that corrupted the JSON analysis output.
Solution: Two-phase architecture + NO_COLOR support
1. Biz/Packages.nix: Pre-builds all packages outside NixOS context
2. Biz.nix: Accepts packages as function argument (default:
Packages.nix) 3. Omni/Bild.nix: Sets NO_COLOR=1 in analysis
derivation 4. Omni/Log/Terminal.hs: Respects NO_COLOR env var
5. Omni/Log/Terminal.hs: Skip getTerminalSize when NO_COLOR set
to avoid
escape code output
6. Omni/Log/Concurrent.hs: Skip line initialization without ANSI
support
Now NixOS builds succeed: - Package IFD happens once at top level -
No recursive builds during service evaluation - Clean JSON output
from bild --plan in Nix sandbox - NixOS configs reference pre-analyzed
packages
Changes: - Add Biz/Packages.nix - standalone package builder - Update
Biz.nix to accept packages argument - Update Biz/Dragons/Analysis.nix
to use Packages.nix - Remove Biz/Targets.nix (replaced by
Packages.nix) - Add NO_COLOR support throughout logging stack -
Fix ANSI.getTerminalSize outputting escape codes when NO_COLOR set
Amp-Thread-ID:
https://ampcode.com/threads/T-bc0f6fc7-46bf-4aa2-892e-dd62e7251d4b
Co-authored-by: Amp <amp@ampcode.com>
| -rwxr-xr-x | Biz.nix | 36 | ||||
| -rwxr-xr-x | Biz/Dragons/Analysis.nix | 35 | ||||
| -rw-r--r-- | Biz/Packages.nix | 15 | ||||
| -rw-r--r-- | Biz/Targets.nix | 19 | ||||
| -rw-r--r-- | Omni/Bild.nix | 1 | ||||
| -rw-r--r-- | Omni/Log/Concurrent.hs | 7 | ||||
| -rw-r--r-- | Omni/Log/Terminal.hs | 16 |
7 files changed, 70 insertions, 59 deletions
@@ -1,16 +1,22 @@ #!/usr/bin/env run.sh # nunya -{bild, ...}: let - # Pre-declared targets prevent recursive builds during NixOS evaluation. - # All bild.run calls happen once at this top level. - targets = import ./Biz/Targets.nix {inherit bild;}; -in - # This is the biz hosting service. Currently it defines a base OS similar to - # Omni/Cloud.nix et al and starts each Biz/* thing as a systemd service. A - # better solution might be to define each Biz/* thing as a container, and then - # wire them together as necessary here, but I don't know how that works so I'll - # just stick to this method for now. - bild.os { +# +# To build the NixOS system: +# 1. First build packages: nix-build Biz/Packages.nix +# 2. Then build OS with packages: nix-build Biz.nix --arg packages "import ./Biz/Packages.nix {}" +# +# Or use the wrapper: Omni/Ide/run.sh Biz.nix +{ + bild, + packages ? import ./Biz/Packages.nix {inherit bild;}, + ... +}: +# This is the biz hosting service. Currently it defines a base OS similar to +# Omni/Cloud.nix et al and starts each Biz/* thing as a systemd service. A +# better solution might be to define each Biz/* thing as a container, and then +# wire them together as necessary here, but I don't know how that works so I'll +# just stick to this method for now. +bild.os { imports = [ ./Omni/Cloud/Hardware.nix ./Omni/Os/Base.nix @@ -25,14 +31,14 @@ in time.timeZone = "America/New_York"; services.storybook = { enable = false; - package = targets.storybook; + package = packages.storybook; }; services.podcastitlater-web = { enable = true; - package = targets.podcastitlater-web; + package = packages.podcastitlater-web; }; services.podcastitlater-worker = { enable = true; - package = targets.podcastitlater-worker; + package = packages.podcastitlater-worker; }; - } +} diff --git a/Biz/Dragons/Analysis.nix b/Biz/Dragons/Analysis.nix index de641e8..b0e0cc9 100755 --- a/Biz/Dragons/Analysis.nix +++ b/Biz/Dragons/Analysis.nix @@ -1,18 +1,19 @@ #!/usr/bin/env run.sh -{bild}: let - targets = import ../Targets.nix {inherit bild;}; -in - # Run this like so: - # - # bild Biz/Dragons/Analysis.nix - # docker load < _/nix/Biz/Dragons/Analysis.nix - # docker run --volume $PWD:/src dragons-analyze dragons-analyze /src/.git - bild.image { - name = "dragons-analyze"; - tag = "latest"; - fromImage = null; - fromImageName = null; - fromImageTag = "latest"; - contents = [bild.pkgs.git targets.dragons-analysis]; - config.Cmd = ["/bin/dragons-analyze"]; - } +{ + bild, + packages ? import ../Packages.nix {inherit bild;}, +}: +# Run this like so: +# +# bild Biz/Dragons/Analysis.nix +# docker load < _/nix/Biz/Dragons/Analysis.nix +# docker run --volume $PWD:/src dragons-analyze dragons-analyze /src/.git +bild.image { + name = "dragons-analyze"; + tag = "latest"; + fromImage = null; + fromImageName = null; + fromImageTag = "latest"; + contents = [bild.pkgs.git packages.dragons-analysis]; + config.Cmd = ["/bin/dragons-analyze"]; +} diff --git a/Biz/Packages.nix b/Biz/Packages.nix new file mode 100644 index 0000000..6b17fe5 --- /dev/null +++ b/Biz/Packages.nix @@ -0,0 +1,15 @@ +# Build all Biz packages independently, outside NixOS context. +# +# This file builds all Biz packages and returns them as an attribute set. +# The NixOS config (Biz.nix) will accept these as inputs rather than +# building them during OS evaluation. +# +# Usage: +# nix-build Biz/Packages.nix # builds all packages +# nix-build Biz/Packages.nix -A storybook # builds one package +{bild ? import ../Omni/Bild.nix {}}: { + storybook = bild.run ../Biz/Storybook.py; + podcastitlater-web = bild.run ../Biz/PodcastItLater/Web.py; + podcastitlater-worker = bild.run ../Biz/PodcastItLater/Worker.py; + dragons-analysis = bild.run ../Biz/Dragons/Analysis.hs; +} diff --git a/Biz/Targets.nix b/Biz/Targets.nix deleted file mode 100644 index 77e462c..0000000 --- a/Biz/Targets.nix +++ /dev/null @@ -1,19 +0,0 @@ -# Pre-declared build targets for the Biz namespace. -# -# This file exposes all buildable Biz targets as an attribute set, allowing -# NixOS configs to reference them directly without triggering recursive builds. -# -# To add a new target: -# 1. Add the attribute here pointing to bild.run ./path/to/target -# 2. Reference it in Biz.nix or other configs as targets.<name> -{bild}: { - # Web services - storybook = bild.run ./Storybook.py; - podcastitlater-web = bild.run ./PodcastItLater/Web.py; - podcastitlater-worker = bild.run ./PodcastItLater/Worker.py; - - # CLI tools and analysis - dragons-analysis = bild.run ./Dragons/Analysis.hs; - - # Add new Biz targets here as they are created -} diff --git a/Omni/Bild.nix b/Omni/Bild.nix index f6291ef..aae82db 100644 --- a/Omni/Bild.nix +++ b/Omni/Bild.nix @@ -229,6 +229,7 @@ + self.lib.strings.removePrefix (toString src) (toString target); buildPhase = '' export CODEROOT=$(pwd) + export NO_COLOR=1 mkdir $out ${self.bild}/bin/bild --plan "$TARGET" 1> $out/analysis.json \ 2> >(tee -a $out/stderr >&2) diff --git a/Omni/Log/Concurrent.hs b/Omni/Log/Concurrent.hs index 83289f3..edf87fd 100644 --- a/Omni/Log/Concurrent.hs +++ b/Omni/Log/Concurrent.hs @@ -79,9 +79,10 @@ withLineManager nss action = do -- | Initialize all lines with pending status initializeLines :: LineManager -> IO () initializeLines LineManager {..} = - case tiMode lmTermInfo of - SingleLine -> pure () -- No initialization needed - MultiLine -> do + case (tiMode lmTermInfo, tiSupportsANSI lmTermInfo) of + (_, False) -> pure () -- No ANSI support, skip initialization + (SingleLine, _) -> pure () -- No initialization needed + (MultiLine, _) -> do nsMap <- readIORef namespaceLines forM_ (Map.toList nsMap) <| \(ns, _) -> do ANSI.hSetCursorColumn IO.stderr 0 diff --git a/Omni/Log/Terminal.hs b/Omni/Log/Terminal.hs index a78e544..0d2ca7a 100644 --- a/Omni/Log/Terminal.hs +++ b/Omni/Log/Terminal.hs @@ -33,16 +33,22 @@ detectTerminal :: IO TerminalInfo detectTerminal = do term <- Env.lookupEnv "TERM" area <- Env.lookupEnv "AREA" + noColor <- Env.lookupEnv "NO_COLOR" -- Check if we support ANSI - let supportsANSI = case (term, area) of - (Just "dumb", _) -> False - (_, Just "Live") -> False -- production logs - (Nothing, _) -> False + let supportsANSI = case (term, area, noColor) of + (_, _, Just _) -> False -- NO_COLOR set + (Just "dumb", _, _) -> False + (_, Just "Live", _) -> False -- production logs + (Nothing, _, _) -> False _ -> True -- Get terminal size, catching exceptions from stdin issues - mSize <- Exception.catch ANSI.getTerminalSize <| \(_ :: Exception.IOException) -> pure Nothing + -- When NO_COLOR is set or ANSI is not supported, skip terminal size detection + -- to avoid outputting escape codes + mSize <- case supportsANSI of + False -> pure Nothing -- Skip if no ANSI support + True -> Exception.catch ANSI.getTerminalSize <| \(_ :: Exception.IOException) -> pure Nothing let (width, height) = case mSize of Just (h, w) -> (w, h) Nothing -> (80, 24) -- sensible default |
