From 1d6b402cdaa1c54b97544f113143af693e795e81 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 23 Jan 2026 16:25:18 -0500 Subject: [PATCH 01/30] internal/scl-singlenode: Make unconfigured state a no-op With the previous state of the module, simply adding the CTRL-OS modules to a NixOS configuration would enable the `easyCerts` system configuration and the `imageRegistry` service. Modules should strive to be no-op by default. The fix is to make the `default` rely on the state of the `services.scl-singlenode.enable` option, since the intent was, AFAICT, to enable those services by default when that option is enabled. Note that using `mkIf (cfg.enable && cfg.easyCerts)` would *work*. At least superficially. This would come with the drawback that it would be impossible to enable those options without enabling the whole `scl-singlenode` option. It might not apply to those options, but *generally speaking*, it is an antipattern, as it couples those tightly with the *implementation* of the service. It then becomes impossible for end-users, whatever their needs are, to enable those options individually. This was checked using a yet-to-be-committed check for no-op correctness of the modules system. Signed-off-by: Samuel Dionne-Riel --- modules/internal/scl-singlenode.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/internal/scl-singlenode.nix b/modules/internal/scl-singlenode.nix index be9bd62..a3d6b6e 100644 --- a/modules/internal/scl-singlenode.nix +++ b/modules/internal/scl-singlenode.nix @@ -92,7 +92,8 @@ in easyCerts = mkOption { description = "Whether to generate TLS and mTLS certificates automatically for the service"; type = types.bool; - default = true; + default = cfg.enable; + defaultText = "config.services.scl-singlenode.enable"; }; logLevel = mkOption { @@ -288,7 +289,8 @@ in imageRegistry = { enable = mkEnableOption "Enable the simple virtual machine image registry" // { - default = true; + default = cfg.enable; + defaultText = "config.services.scl-singlenode.enable"; }; dataDir = mkOption { From b2cbca8396a12b10e9726d6df7bbced3db870d3f Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 25 Jan 2026 13:59:35 -0500 Subject: [PATCH 02/30] lib: Add check-modules-no-ops This expression checks that, as much as it can check, the evaluations of the modules system are equivalent with added modules. This is important when producing modules to import arbitrarily in your config, as they shouldn't change the behaviour of your system until they are enabled. Signed-off-by: Samuel Dionne-Riel --- lib/check-modules-no-ops.nix | 261 +++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 lib/check-modules-no-ops.nix diff --git a/lib/check-modules-no-ops.nix b/lib/check-modules-no-ops.nix new file mode 100644 index 0000000..fe24fb7 --- /dev/null +++ b/lib/check-modules-no-ops.nix @@ -0,0 +1,261 @@ +# Instantiating against an arbitrary Nixpkgs: +# $ nix-instantiate --attr result ./lib/check-modules-no-ops.nix --arg pkgs 'import {}' +# Using in a REPL with an arbitrary Nixpkgs: +# $ nix repl -f ./lib/check-modules-no-ops.nix --arg pkgs 'import {}' +{ + pkgs, + + # Base configuration compared against. + # There shouldn't be any need to change this with a NixOS evaluation. + baseCfg ? + let + inherit (pkgs) lib; + in + { + # Prevent stray warnings in the output. + system.stateVersion = lib.mkDefault "00.00"; + + # Manual builds will differ since new options are added. This is okay. + documentation.nixos.enable = lib.mkDefault false; + + # Minimum config for evaluation purpose. + # `boot.isContainer` will not work as hoped. + fileSystems."/".device = lib.mkDefault "nodev"; + boot.loader.grub.devices = lib.mkDefault [ "nodev" ]; + nixpkgs.hostPlatform = lib.mkDefault pkgs.stdenv.hostPlatform.system; + }, + + # Function that takes a `config` attribute set or function, and evaluates + # a Modules system returning the bare attribute set from the evaluation. + evalConfig ? + config: + (import (pkgs.path + "/nixos")) { + configuration = { + imports = [ + config + baseCfg + ]; + }; + # Use Flakes-required semantics around NixOS `system` argument. + system = null; + }, + + # The modules being checked. Like `nixosModules` in Flakes. + modules ? import ../modules, +}: + +let + inherit (pkgs) lib; + + # `walkOptions` applies `fn` to all “options” found in the `options` + # attribute set of a NixOS module system evaluation. + # Think of it like “`mapAttrs` but only for the option leaf nodes”. + walkOptions = + fn: options: + walkOptions' { + inherit fn; + options = + # Cleanup `options` eagerly. + options // { + meta = builtins.removeAttrs (options.meta or { }) [ + # meta.maintainers historically may contain bogus entries that abort evaluation. + # This is problematic for no-op checking, so don't check for those. + "maintainers" + ]; + }; + }; + + # Implementation details for `walkOptions`. + walkOptions' = + { + options, + path ? [ ], + fn, + }: + builtins.mapAttrs ( + name: value: + let + currPath = path ++ [ name ]; + in + if builtins.isAttrs value && (value._type or null != "option") then + walkOptions' { + options = value; + path = currPath; + inherit fn; + } + else + fn currPath value + ) options; + + # Takes a module system option tree, and walk through it to simplify its + # structure in a way that evaluates around the values being evaluated. + # While incomplete, this provides a way to compare two different module + # system evaluation. + # It will be lacking some definition locations in `definitionLocations'`. + # At the same time, this will catch (through `highestPrio`) those where the + # definitions differ but couldn't be listed. Except for cases where both + # the declarations can't be listed and the definitions have the same + # priority, such as attribute sets and lists. + # TIP: To further confirm no-op-ness, evaluate a "holistic" value, like the + # `system.build.toplevel` or other similar results. + simplifyOptions = walkOptions ( + _path: option: + let + result = builtins.tryEval ( + let + values = rec { + # Evaluating `default` may fail, and that's expected. + hasDefault = option ? default; + # Some definition locations that we can evaluate. + # Note that `defaultText` *very often* implies the option will not evaluate + # when the modules is not configured entirely. So skip those values... + definitionLocations' = + if option ? defaultText then + "(skipped possibly un-evaluatable option...)" + #then "(skipped internal)" + else + option.files; + # Directly inherit values we can inherit. + inherit (option) + declarationPositions + highestPrio + options + ; + + # The type *technically* can be changed, but is generally awkward to + # compare, so pick a few representative values out. + type = { + inherit (option.type) + _type + description + name + ; + } + // (lib.optionalAttrs (option.type ? internal) { inherit (option) internal; }); + + # While untrue, this allows us to call `walkOptions` to walk this simplified tree. + _type = "option"; + } + // (lib.optionalAttrs (option ? description) { inherit (option) description; }) + // (lib.optionalAttrs (option ? defaultText) { inherit (option) defaultText; }); + in + # Fail eagerly, or else laziness will make this fail outside the `tryEval`. + builtins.deepSeq (builtins.attrValues values) values + ); + in + if result.success then + result.value + # We treat (catchable) errors as being a comparable value. + # Any error is equal to another error. + # There won't be uncatchable errors in a correct module. + else + "« error at ${option} »" + ); + + # Compare NixOS module system `optionsA` and `optionsB`, returning the + # options from `optionsB`, keeping only the options found in `optionsA`. + # This is used to remove the added options from the new modules in the + # `optionsB` evaluation. + filterExistingOptionsFrom = + optionsA: optionsB: + + walkOptions ( + path: _: lib.attrByPath path "« Options ${builtins.toJSON path} missing in optionsB... »" optionsB + ) optionsA; + + # Returns an attribute set with `{ a = ...; b = ...; }` where `a` is the + # `simplifyOptions optionsA` and `b` is the result of keeping only the common + # options with `optionsA` after `simplifyOptions optionsB`. + # This result is what gets compared. + prepareOptionsForComparison = + optionsA: optionsB: + + rec { + a = simplifyOptions optionsA; + b = filterExistingOptionsFrom a (simplifyOptions optionsB); + }; + + compareEvals = + name: evalA: evalB: + + let + toplevelA = builtins.unsafeDiscardStringContext evalA.config.system.build.toplevel.drvPath; + toplevelB = builtins.unsafeDiscardStringContext evalB.config.system.build.toplevel.drvPath; + results = + builtins.filter (result: result != null) + # NOTE: we're not using multi-line strings `''` for these messages + # since they include a final `\n`, and indentation management can be wonky. + [ + ( + if toplevelA != toplevelB then + builtins.concatStringsSep "\n" [ + " - system.build.toplevel does not match:" + " ${toplevelA} != ${toplevelB}" + ] + else + null + ) + ( + let + inherit (prepareOptionsForComparison evalA.options evalB.options) a b; + jsonA = builtins.toFile "evalA.json" (builtins.toJSON a); + jsonB = builtins.toFile "evalB.json" (builtins.toJSON b); + in + if (a != b) then + builtins.concatStringsSep "\n" [ + " - Module system evaluations differ" + " ${jsonA} != ${jsonB}" + " Tip: use `jdiff --syntax rightonly --indent 2 ${jsonA} ${jsonB}` to compare." + ] + else + null + ) + ]; + in + if results != [ ] then + [ + '' + Module '${toString name}' on ${evalA.pkgs.stdenv.hostPlatform.system} is not a no-op. + ${builtins.concatStringsSep "\n" results} + '' + ] + else + [ ]; + + # The default evaluation being compared against, without added modules. + defaultEval = evalConfig { }; + + # Given an attrset `{ moduleName = importedModule; }`, check each module + # and throw with a list of module issues. + checkModules = + modules: + let + results = builtins.concatLists ( + lib.mapAttrsToList (name: module: compareEvals name defaultEval (evalConfig module)) modules + ); + in + if results != [ ] then builtins.throw (lib.concatStringsSep "\n" results) else true; + + # The trivial-to-access result value + result = checkModules modules; +in +# We are exposing some of the functions so this can be more easily +# diagnosed withing a `nix repl` invocation. +{ + inherit + # Functions + checkModules + evalConfig + filterExistingOptionsFrom + prepareOptionsForComparison + simplifyOptions + walkOptions + + # Values + modules + defaultEval + + # Result + result + ; +} From 6f5f8db990125d4684bcc42c59eef0cf48cb702a Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 25 Jan 2026 14:05:33 -0500 Subject: [PATCH 03/30] modules: Drop unneeded version gating Signed-off-by: Samuel Dionne-Riel --- flake.nix | 2 +- modules/default.nix | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index 96119eb..cd10c9a 100644 --- a/flake.nix +++ b/flake.nix @@ -32,7 +32,7 @@ ./checks/pre-commit.nix ]; - flake.nixosModules = import ./modules inputs.nixpkgs.lib; + flake.nixosModules = import ./modules; perSystem = { diff --git a/modules/default.nix b/modules/default.nix index 6593465..6ac336d 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,7 +1,4 @@ -lib: { developer = import ./developer.nix; -} -// lib.optionalAttrs (lib.versionAtLeast lib.version "25.11") { vms = import ./vms.nix; } From f3282afb48a2d76fb93c28db072982b6f8890a6b Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 25 Jan 2026 14:23:26 -0500 Subject: [PATCH 04/30] checks: Only run vms check on plausible systems Signed-off-by: Samuel Dionne-Riel --- checks/default.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/checks/default.nix b/checks/default.nix index 9f4f691..f9fc45f 100644 --- a/checks/default.nix +++ b/checks/default.nix @@ -8,7 +8,12 @@ } // inputs.nixpkgs.lib.optionalAttrs - (inputs.nixpkgs.lib.versionAtLeast inputs.nixpkgs.lib.version "25.11") + ( + inputs.nixpkgs.lib.versionAtLeast inputs.nixpkgs.lib.version "25.11" + && pkgs.stdenv.isLinux + # Package ‘vm-test-run-vms’ [...] is not available on the requested hostPlatform: hostPlatform.system = "aarch64-linux" + && pkgs.stdenv.isx86_64 + ) { vms = pkgs.callPackage ./vms.nix { inherit (self) nixosModules; }; }; From 77fb0e1451d7cf583ccca6a09c57d6b46a790791 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 25 Jan 2026 14:24:10 -0500 Subject: [PATCH 05/30] checks: Add modules checks for Linux evals Signed-off-by: Samuel Dionne-Riel --- checks/default.nix | 3 +++ checks/modules.nix | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 checks/modules.nix diff --git a/checks/default.nix b/checks/default.nix index f9fc45f..4535f29 100644 --- a/checks/default.nix +++ b/checks/default.nix @@ -6,6 +6,9 @@ checks = { developer = pkgs.callPackage ./developer.nix { inherit (self) nixosModules; }; } + // inputs.nixpkgs.lib.optionalAttrs (pkgs.stdenv.isLinux) { + modules = pkgs.callPackage ./modules.nix { inherit (self) nixosModules; }; + } // inputs.nixpkgs.lib.optionalAttrs ( diff --git a/checks/modules.nix b/checks/modules.nix new file mode 100644 index 0000000..bf86e10 --- /dev/null +++ b/checks/modules.nix @@ -0,0 +1,14 @@ +{ + pkgs, + nixosModules, +}: + +let + check = import ../lib/check-modules-no-ops.nix { + inherit pkgs; + modules = nixosModules; + }; +in +builtins.seq check.result ( + (pkgs.writeText "modules-check" (builtins.toJSON check.result)) // { inherit check; } +) From a322a43e800506e1193b6f7376722fa6c26aa332 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 23 Jan 2026 12:09:29 -0500 Subject: [PATCH 06/30] modules: Drop developer, create profiles The "profiles" modules will each have a global option to enable all the opinionated options, and sub-options to make it possible for opting-in or opting-out of the opinionated settings. While it looks redundant to have the `developer` profile simply mirror the `ctrl-os-system` profile, the intent is that the developer system may enable options that are irrelevant on an "target" system. For example, udev configuration to work with USB download mode for target hardware devices. Conversely, later down the line it may make sense to enable options for the target systems that a developer generally wouldn't want. For now, we're re-using the ctrl-os-system options as it makes more sense to implement them there, since the installed systems *should* have that profile enabled to work as designed. --- modules/default.nix | 2 +- modules/profiles/README.md | 56 +++++++++++++++++++++ modules/profiles/ctrl-os-system/default.nix | 53 +++++++++++++++++++ modules/profiles/default.nix | 6 +++ modules/profiles/developer/default.nix | 32 ++++++++++++ 5 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 modules/profiles/README.md create mode 100644 modules/profiles/ctrl-os-system/default.nix create mode 100644 modules/profiles/default.nix create mode 100644 modules/profiles/developer/default.nix diff --git a/modules/default.nix b/modules/default.nix index 6ac336d..5e2178f 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,4 +1,4 @@ { - developer = import ./developer.nix; + profiles = import ./profiles; vms = import ./vms.nix; } diff --git a/modules/profiles/README.md b/modules/profiles/README.md new file mode 100644 index 0000000..ba2a74f --- /dev/null +++ b/modules/profiles/README.md @@ -0,0 +1,56 @@ +Profiles +======== + +Profiles implement collection of settings that are generally enabled together. + +Since some options can be awkward to undo, it is important that the many *logical settings* that are set can be disabled. + +The pattern used for profiles is the following: + +```nix +{ config, lib, ... }: + +let + cfg = config.ctrl-os.profiles.${profileName}; + # Makes an "enable" option that defaults to the `${profileName}.enable` state. + mkDefaultEnable = description: + (lib.mkEnableOption description) // { + default = cfg.enable; + defaultText = "config.ctrl-os.profiles.${profileName}.enable"; + } + ; +{ + options = { + ctrl-os.profiles.${profileName} = { + enable = lib.mkEnableOption "the [...] options"; + # Then, as needed, discrete options for the collection. + useFoo = mkDefaultEnable "enabling systemd-wide usage of foo"; + disableBar = mkDefaultEnable "disabling systemd-wide usage of bar"; + # etc... + }; + }; + config = lib.mkMerge [ + (lib.mkIf cfg.useFoo { + services.foo.enable = true; + }); + (lib.mkIf cfg.disableBar { + services.bar.enable = false; + }); + ]; +} +``` + +It is important not to make the logic *weird* with the logical settings: they must ***all*** resolve to `cfg.enable` as their default value. +When usage of the profile is not enabled, including the options module should be a no-op. + +In the previous example, making `useBar` rather than `disableBar` would have required making the option a negation, or making the implementation weird by checking both for `cfg.enable` and `cfg.useBar`. + +### Why make discrete options when you can `mkForce`? + +There are two main reasons. +First, this makes it possible for users of the modules to enable only a logical configuration from our module without enabling the whole profile. + +Then, this also helps ensure we are not forcibly clobbering `mkDefault` or other `mkOverride` levels from the user's config when they want to enable a whole profile except one option. +This is especially needed for `listOf` type options, or `attrsOf` that may `mkMerge` badly, or unexpectedly. + +Whether opting-in or opting-out is desired, both are important escape hatches for our modules users. diff --git a/modules/profiles/ctrl-os-system/default.nix b/modules/profiles/ctrl-os-system/default.nix new file mode 100644 index 0000000..b713160 --- /dev/null +++ b/modules/profiles/ctrl-os-system/default.nix @@ -0,0 +1,53 @@ +{ config, lib, ... }: + +let + cfg = config.ctrl-os.profiles.ctrl-os-system; + + # Makes an "enable" option that defaults to the `ctrl-os-system.enable` state. + mkDefaultEnable = + description: + (lib.mkEnableOption description) + // { + default = cfg.enable; + defaultText = "config.ctrl-os.profiles.ctrl-os-system.enable"; + }; +in +{ + options = { + ctrl-os.profiles.ctrl-os-system = { + enable = lib.mkEnableOption "the opinionated settings for an installed CTRL-OS system"; + # NOTE: The following module logical settings are re-used in other modules. + useFlakes = mkDefaultEnable "system-wide usage of Flakes"; + useCache = mkDefaultEnable "system-wide usage of the CTRL-OS binary cache"; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.useCache { + nix = { + settings = { + extra-trusted-public-keys = [ + "ctrl-os:baPzGxj33zp/P+GAIJXsr8ss9Law+qEEFViX1+flbv8=" + ]; + + extra-substituters = [ + "https://cache.ctrl-os.com/" + ]; + }; + }; + }) + (lib.mkIf cfg.useFlakes { + nix = { + settings = { + # While some developers prefer not to use flakes for their + # projects, it is convenient to have them enabled to + # copy'n'paste documentation snippets. + experimental-features = [ + "nix-command" + "flakes" + ]; + }; + }; + }) + ]; +} diff --git a/modules/profiles/default.nix b/modules/profiles/default.nix new file mode 100644 index 0000000..1ee5d5c --- /dev/null +++ b/modules/profiles/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./developer + ./ctrl-os-system + ]; +} diff --git a/modules/profiles/developer/default.nix b/modules/profiles/developer/default.nix new file mode 100644 index 0000000..eabd643 --- /dev/null +++ b/modules/profiles/developer/default.nix @@ -0,0 +1,32 @@ +{ config, lib, ... }: + +let + cfg = config.ctrl-os.profiles.developer; + + # Makes an "enable" option that defaults to the `developer.enable` state. + mkDefaultEnable = + description: + (lib.mkEnableOption description) + // { + default = cfg.enable; + defaultText = "config.ctrl-os.profiles.developer.enable"; + }; +in +{ + options = { + ctrl-os.profiles.developer = { + enable = lib.mkEnableOption "the opinionated CTRL-OS developer settings"; + useFlakes = mkDefaultEnable "system-wide usage of Flakes"; + useCache = mkDefaultEnable "system-wide usage of the CTRL-OS binary cache"; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.useCache { + ctrl-os.profiles.ctrl-os-system.useCache = true; + }) + (lib.mkIf cfg.useFlakes { + ctrl-os.profiles.ctrl-os-system.useFlakes = true; + }) + ]; +} From 81d91a593bb0103c6831fd574122003c39aa7058 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 23 Jan 2026 12:13:01 -0500 Subject: [PATCH 07/30] checks/developer: Use the new profile --- checks/developer.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/checks/developer.nix b/checks/developer.nix index b93d78b..ab71b59 100644 --- a/checks/developer.nix +++ b/checks/developer.nix @@ -1,11 +1,11 @@ { nixosModules, testers }: testers.nixosTest { - name = "developer"; + name = "profiles.developer"; nodes.machine = { - imports = [ nixosModules.developer ]; + imports = [ nixosModules.profiles ]; - ctrl-os.developer.enable = true; + ctrl-os.profiles.developer.enable = true; }; testScript = '' From bd4068e7ab131a2621152090c827823fa9846341 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 25 Jan 2026 14:45:23 -0500 Subject: [PATCH 08/30] =?UTF-8?q?README:=20developer=20=E2=86=92=20profile?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b74453..dd19f3b 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ status symbols: | Module | Status | Unstable | 26.05 | 24.05 | Description | |--------------------------------------------------------------------|----------|----------|-------|-------|----------------------------------------------| -| [`developer`](https://docs.ctrl-os.com/modules/ctrl-os-developer/) | **Beta** | ✅ | 🚧 | ✅ | Useful settings for developers using CTRL-OS | +| [`profiles`](https://docs.ctrl-os.com/modules/ctrl-os-profiles/) | **Beta** | ✅ | 🚧 | ✅ | Different opinionated settings for CTRL-OS | | [`vms`](https://docs.ctrl-os.com/modules/ctrl-os-vms/) | **Beta** | ✅ | 🚧 | ❌ | Declarative way to run generic VMs | ## Hardware Support From 4141bf36b6165981db9ab51e844c2a8e6287d7de Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 25 Jan 2026 14:45:39 -0500 Subject: [PATCH 09/30] README: Drop the `ctrl-os.*.enable` hint We can't do that. Plain and simple. Already the hardware support is not following that model, and the new profiles aren't either. This is even more important when thinking about future-proofing the "API" of our modules: we don't want tons and tons of options at the root, it makes for a messy user experience. Instead, broader scopes for modules should be defined, and those scopes define the options that make sense for them. --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dd19f3b..e8b44a2 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,8 @@ All modules are available via `nixosModules` of this Flake. If you don't use Flakes, import the module file in `/modules` directly. We will streamline this later! -Modules follow a simple configuration pattern. Module `foo` has its -configuration under `ctrl-os.foo`. So to enable module `foo`, you -typically write `ctrl-os.foo.enable = true`; +Modules have different purposes and semantics, and thus interfaces. Read the +usage for your chosen modules for more details about their use. ## Available Modules From a2bd52cf0e3e5d578c114553649468ee98d20b18 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 23 Jan 2026 15:31:41 -0500 Subject: [PATCH 10/30] NO-NOT-MERGE: fix out-of-tree docs for developer module From 8b7c468a30b6659a8552258d9d0b7b1febc20fa2 Mon Sep 17 00:00:00 2001 From: Martin Messer Date: Fri, 9 Jan 2026 13:16:49 +0100 Subject: [PATCH 11/30] modules: add Nvidia Jetson Nano Boot Module --- modules/default.nix | 1 + modules/platform.nix | 16 ++++++++++++++++ modules/platforms/nvidiaJetsonOrinNano.nix | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 modules/platform.nix create mode 100644 modules/platforms/nvidiaJetsonOrinNano.nix diff --git a/modules/default.nix b/modules/default.nix index 5e2178f..1b7dcb1 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,4 +1,5 @@ { + platform = import ./platform.nix; profiles = import ./profiles; vms = import ./vms.nix; } diff --git a/modules/platform.nix b/modules/platform.nix new file mode 100644 index 0000000..b0ea86e --- /dev/null +++ b/modules/platform.nix @@ -0,0 +1,16 @@ +{ lib, ... }: +{ + options.ctrl-os.platform = lib.mkOption { + type = + with lib.types; + nullOr (enum [ + "nvidia-jetson-orin-nano" + ]); + description = "The platform, we are running on."; + default = null; + }; + + imports = [ + ./platforms/nvidiaJetsonOrinNano.nix + ]; +} diff --git a/modules/platforms/nvidiaJetsonOrinNano.nix b/modules/platforms/nvidiaJetsonOrinNano.nix new file mode 100644 index 0000000..10c4b6e --- /dev/null +++ b/modules/platforms/nvidiaJetsonOrinNano.nix @@ -0,0 +1,19 @@ +{ config, lib, ... }: +let + platform = config.ctrl-os.platform; +in +{ + config = lib.mkIf (platform == "nvidia-jetson-orin-nano") { + + nixpkgs.hostPlatform = "aarch64-linux"; + + boot.initrd.availableKernelModules = [ + # Enable PCIe support at boot time + "phy_tegra194_p2u" + "pcie_tegra194" + # Enable USB support for USB Boot + "xhci-tegra" + "phy-tegra-xusb" + ]; + }; +} From faca49c371404c79006b9e72d4e2f79b8064eccc Mon Sep 17 00:00:00 2001 From: Martin Messer Date: Fri, 9 Jan 2026 13:19:11 +0100 Subject: [PATCH 12/30] packages: add packages definition and NVidia Jetson Orin installer --- packages/flakeModule.nix | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/flakeModule.nix b/packages/flakeModule.nix index 64ed189..31fbe93 100644 --- a/packages/flakeModule.nix +++ b/packages/flakeModule.nix @@ -1,4 +1,9 @@ -{ withSystem, inputs, ... }: +{ + withSystem, + inputs, + self, + ... +}: { } // inputs.nixpkgs.lib.optionalAttrs @@ -18,8 +23,30 @@ }; perSystem = - { pkgs, ... }: + { pkgs, system, ... }: { - packages = import ./default.nix { inherit pkgs; }; + packages = (import ./default.nix { inherit pkgs; }) // { + jetsonOrinNanoInstaller = + (inputs.nixpkgs.lib.nixosSystem { + modules = [ + ( + { modulesPath, ... }: + { + imports = [ + "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" + self.nixosModules.platform + self.nixosModules.profiles + ]; + + ctrl-os.profiles.developer.enable = true; + ctrl-os.platform = "nvidia-jetson-orin-nano"; + nixpkgs.hostPlatform = "aarch64-linux"; + nixpkgs.buildPlatform = system; + system.stateVersion = "25.11"; + } + ) + ]; + }).config.system.build.isoImage; + }; }; } From 6a6d1e6715b1067dd7cbed48dbded0c6ed3a7760 Mon Sep 17 00:00:00 2001 From: Martin Messer Date: Fri, 9 Jan 2026 14:53:10 +0100 Subject: [PATCH 13/30] flake: update dependencies --- flake.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/flake.lock b/flake.lock index c3832e5..0c359d3 100644 --- a/flake.lock +++ b/flake.lock @@ -3,15 +3,15 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1747046372, - "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", - "owner": "edolstra", + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", "repo": "flake-compat", - "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { - "owner": "edolstra", + "owner": "NixOS", "repo": "flake-compat", "type": "github" } @@ -21,11 +21,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1762980239, - "narHash": "sha256-8oNVE8TrD19ulHinjaqONf9QWCKK+w4url56cdStMpM=", + "lastModified": 1767609335, + "narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "52a2caecc898d0b46b2b905f058ccc5081f842da", + "rev": "250481aafeb741edfe23d29195671c19b36b6dca", "type": "github" }, "original": { @@ -57,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1763421233, - "narHash": "sha256-re7SYzc1gs7069DojQiW90fPjNfCUJ9+RUEplQPqxDs=", - "rev": "89c2b2330e733d6cdb5eae7b899326930c2c0648", + "lastModified": 1767767207, + "narHash": "sha256-LmdqDciUNW4a3tFH539SorGLD/5t8PFOXhEQlIxbtww=", + "rev": "5912c1772a44e31bf1c63c0390b90501e5026886", "type": "tarball", - "url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre897910.89c2b2330e73/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre923638.5912c1772a44/nixexprs.tar.xz" }, "original": { "type": "tarball", @@ -70,11 +70,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1761765539, - "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=", + "lastModified": 1765674936, + "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc", + "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", "type": "github" }, "original": { @@ -92,11 +92,11 @@ ] }, "locked": { - "lastModified": 1763319842, - "narHash": "sha256-YG19IyrTdnVn0l3DvcUYm85u3PaqBt6tI6VvolcuHnA=", + "lastModified": 1767281941, + "narHash": "sha256-6MkqajPICgugsuZ92OMoQcgSHnD6sJHwk8AxvMcIgTE=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "7275fa67fbbb75891c16d9dee7d88e58aea2d761", + "rev": "f0927703b7b1c8d97511c4116eb9b4ec6645a0fa", "type": "github" }, "original": { From 4068058e4220d13e5c1d76a17232836ba52b7615 Mon Sep 17 00:00:00 2001 From: Martin Messer Date: Fri, 16 Jan 2026 15:18:32 +0100 Subject: [PATCH 14/30] github-workflow: add installer iso build process --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bab5a4e..4081f6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,15 @@ jobs: fi - name: Build the Modules for ${{ matrix.nixOpts.tag }} run: nix -L flake check ${{ matrix.nixOpts.flag }} + - name: Build the NixOS installer for Nvidia Jetson Orin Nano devices + run: nix -L build .#packages.x86_64-linux.jetsonOrinNanoInstaller + - name: Upload the installer as artifact + uses: actions/upload-artifact@v6 + with: + name: nvidia-jetson-orin-nano-nixos-${{ github.sha }} + path: result/iso/nixos-minimal-*.iso + retention-days: 7 + if-no-files-found: error - name: Export Nix Store if: always() run: | From 2e4bd081c77ea35ec17d2bc4f2f01603ff43e127 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Thu, 22 Jan 2026 13:02:49 -0500 Subject: [PATCH 15/30] lib: Init with getVendorsModules and mapDirs Signed-off-by: Samuel Dionne-Riel --- lib/default.nix | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 lib/default.nix diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..64e8ff3 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,47 @@ +# A `nixpkgs.lib` compatible lib must be provided. +{ lib }: + +let + self = { + /** + Applies the function `fn` on the direct children directories of `dir`. + */ + mapDirs = + fn: dir: + let + dirs = builtins.map (name: fn (dir + "/${name}")) ( + builtins.attrNames (lib.filterAttrs (_name: type: type == "directory") (builtins.readDir dir)) + ); + in + dirs; + + /** + Given a directory where children are structured by vendors and *thing*, + returns an attribute set with combined `"${vendor}-${thing}"` names, + and values representing that subdirectory. + */ + getVendorsModules = + dir: + builtins.listToAttrs ( + builtins.concatLists ( + self.mapDirs ( + vendorDir: + let + vendor = builtins.baseNameOf vendorDir; + in + self.mapDirs ( + thingDir: + let + thing = builtins.baseNameOf thingDir; + in + { + name = "${vendor}-${thing}"; + value = thingDir; + } + ) vendorDir + ) dir + ) + ); + }; +in +self From 1c68e52b0c159bbf6472387c9bdc1772f514f362 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Thu, 22 Jan 2026 12:48:53 -0500 Subject: [PATCH 16/30] treewide: Fix jetson-orin-nano-super name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The device name (even though the vendor does not seem to always spell it out), includes “Super”. I'm using the fact that the innate DeviceTree for the platform uses the model name: ``` NVIDIA Jetson Orin Nano Engineering Reference Developer Kit Super ``` The `compatible` is `nvidia,p3768-0000+p3767-0005-super`. For this reason, we shall always use “super”. Signed-off-by: Samuel Dionne-Riel --- modules/platform.nix | 4 ++-- .../nvidia/jetson-orin-nano-super/default.nix} | 2 +- packages/flakeModule.nix | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename modules/platforms/{nvidiaJetsonOrinNano.nix => devices/nvidia/jetson-orin-nano-super/default.nix} (83%) diff --git a/modules/platform.nix b/modules/platform.nix index b0ea86e..0779e9b 100644 --- a/modules/platform.nix +++ b/modules/platform.nix @@ -4,13 +4,13 @@ type = with lib.types; nullOr (enum [ - "nvidia-jetson-orin-nano" + "nvidia-jetson-orin-nano-super" ]); description = "The platform, we are running on."; default = null; }; imports = [ - ./platforms/nvidiaJetsonOrinNano.nix + ./platforms/devices/nvidia/jetson-orin-nano-super/default.nix ]; } diff --git a/modules/platforms/nvidiaJetsonOrinNano.nix b/modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix similarity index 83% rename from modules/platforms/nvidiaJetsonOrinNano.nix rename to modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix index 10c4b6e..3585579 100644 --- a/modules/platforms/nvidiaJetsonOrinNano.nix +++ b/modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix @@ -3,7 +3,7 @@ let platform = config.ctrl-os.platform; in { - config = lib.mkIf (platform == "nvidia-jetson-orin-nano") { + config = lib.mkIf (platform == "nvidia-jetson-orin-nano-super") { nixpkgs.hostPlatform = "aarch64-linux"; diff --git a/packages/flakeModule.nix b/packages/flakeModule.nix index 31fbe93..aa4c945 100644 --- a/packages/flakeModule.nix +++ b/packages/flakeModule.nix @@ -39,7 +39,7 @@ ]; ctrl-os.profiles.developer.enable = true; - ctrl-os.platform = "nvidia-jetson-orin-nano"; + ctrl-os.platform = "nvidia-jetson-orin-nano-super"; nixpkgs.hostPlatform = "aarch64-linux"; nixpkgs.buildPlatform = system; system.stateVersion = "25.11"; From 7ba7359fd980823045c9d722d9dc36a3596e47f4 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Thu, 22 Jan 2026 12:51:20 -0500 Subject: [PATCH 17/30] modules/platform: Make generic and self-describing via structure Signed-off-by: Samuel Dionne-Riel --- modules/platform.nix | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/modules/platform.nix b/modules/platform.nix index 0779e9b..4f6cf84 100644 --- a/modules/platform.nix +++ b/modules/platform.nix @@ -1,16 +1,20 @@ { lib, ... }: + +let + inherit + (import ../lib { inherit lib; }) + getVendorsModules + ; + deviceModules = getVendorsModules ./platforms/devices; + devices = builtins.attrNames deviceModules; + deviceDirs = builtins.attrValues deviceModules; +in { options.ctrl-os.platform = lib.mkOption { - type = - with lib.types; - nullOr (enum [ - "nvidia-jetson-orin-nano-super" - ]); + type = with lib.types; nullOr (enum devices); description = "The platform, we are running on."; default = null; }; - imports = [ - ./platforms/devices/nvidia/jetson-orin-nano-super/default.nix - ]; + imports = deviceDirs; } From 1637be629b7853b5f918fa994a667e97eb46c345 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Thu, 22 Jan 2026 18:46:28 -0500 Subject: [PATCH 18/30] modules/platform: Make platform module handle gating off modules This will ensure the `mkIf` conditional is always *coherent* with what it should be. Otherwise, copy/paste mistakes can become awkward to debug. Signed-off-by: Samuel Dionne-Riel --- modules/platform.nix | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/modules/platform.nix b/modules/platform.nix index 4f6cf84..58aa408 100644 --- a/modules/platform.nix +++ b/modules/platform.nix @@ -1,13 +1,13 @@ -{ lib, ... }: +{ config, lib, ... }@moduleArgs: let + platform = config.ctrl-os.platform; inherit (import ../lib { inherit lib; }) getVendorsModules ; deviceModules = getVendorsModules ./platforms/devices; devices = builtins.attrNames deviceModules; - deviceDirs = builtins.attrValues deviceModules; in { options.ctrl-os.platform = lib.mkOption { @@ -16,5 +16,35 @@ in default = null; }; - imports = deviceDirs; + imports = + builtins.attrValues + ( + builtins.mapAttrs + ( + device: dir: + let + cfgOrFn = import dir; + appliedConfig = + let config = cfgOrFn moduleArgs; in + if config ? config then + config + else + { inherit config; } + ; + cond = lib.mkIf (platform == device); + in + if builtins.isAttrs cfgOrFn then + { + config = cond ( + cfgOrFn + ); + } + else + appliedConfig // { + config = cond appliedConfig.config; + } + ) + deviceModules + ) + ; } From 144e3ff4d885e95389b5e8fea7e8a1ede5a3a5bf Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Thu, 22 Jan 2026 19:58:51 -0500 Subject: [PATCH 19/30] devices/nvidia/jetson-orin-nano-super: Remove gating from module Signed-off-by: Samuel Dionne-Riel --- .../nvidia/jetson-orin-nano-super/default.nix | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix b/modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix index 3585579..8a00138 100644 --- a/modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix +++ b/modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix @@ -1,19 +1,14 @@ -{ config, lib, ... }: -let - platform = config.ctrl-os.platform; -in -{ - config = lib.mkIf (platform == "nvidia-jetson-orin-nano-super") { +{ lib, ... }: - nixpkgs.hostPlatform = "aarch64-linux"; +{ + nixpkgs.hostPlatform = "aarch64-linux"; - boot.initrd.availableKernelModules = [ - # Enable PCIe support at boot time - "phy_tegra194_p2u" - "pcie_tegra194" - # Enable USB support for USB Boot - "xhci-tegra" - "phy-tegra-xusb" - ]; - }; + boot.initrd.availableKernelModules = [ + # Enable PCIe support at boot time + "phy_tegra194_p2u" + "pcie_tegra194" + # Enable USB support for USB Boot + "xhci-tegra" + "phy-tegra-xusb" + ]; } From 3ca841558b85432869ce7edb5619ec4fe7d2940d Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 23 Jan 2026 11:15:20 -0500 Subject: [PATCH 20/30] hardware: Reorganize modules directory structure This is the first part of a change set that mirrors a commit from Julian. This only *moves* the files and module itself around, the options names are changed in the follow-up commit. Co-authored-by: Julian Stecklina Signed-off-by: Samuel Dionne-Riel --- modules/default.nix | 2 +- modules/hardware/default.nix | 5 +++++ modules/{platform.nix => hardware/devices/default.nix} | 6 +++--- .../devices/nvidia/jetson-orin-nano-super/default.nix | 2 -- 4 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 modules/hardware/default.nix rename modules/{platform.nix => hardware/devices/default.nix} (91%) rename modules/{platforms => hardware}/devices/nvidia/jetson-orin-nano-super/default.nix (94%) diff --git a/modules/default.nix b/modules/default.nix index 1b7dcb1..8f272ac 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,5 +1,5 @@ { - platform = import ./platform.nix; + hardware = import ./hardware; profiles = import ./profiles; vms = import ./vms.nix; } diff --git a/modules/hardware/default.nix b/modules/hardware/default.nix new file mode 100644 index 0000000..605f052 --- /dev/null +++ b/modules/hardware/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./devices + ]; +} diff --git a/modules/platform.nix b/modules/hardware/devices/default.nix similarity index 91% rename from modules/platform.nix rename to modules/hardware/devices/default.nix index 58aa408..79a0f89 100644 --- a/modules/platform.nix +++ b/modules/hardware/devices/default.nix @@ -3,10 +3,10 @@ let platform = config.ctrl-os.platform; inherit - (import ../lib { inherit lib; }) + (import ../../../lib { inherit lib; }) getVendorsModules - ; - deviceModules = getVendorsModules ./platforms/devices; + ; + deviceModules = getVendorsModules ./.; devices = builtins.attrNames deviceModules; in { diff --git a/modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix b/modules/hardware/devices/nvidia/jetson-orin-nano-super/default.nix similarity index 94% rename from modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix rename to modules/hardware/devices/nvidia/jetson-orin-nano-super/default.nix index 8a00138..a6b0412 100644 --- a/modules/platforms/devices/nvidia/jetson-orin-nano-super/default.nix +++ b/modules/hardware/devices/nvidia/jetson-orin-nano-super/default.nix @@ -1,5 +1,3 @@ -{ lib, ... }: - { nixpkgs.hostPlatform = "aarch64-linux"; From b022c2c325c7d1d6d4773312c7596608d7c43ae9 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 23 Jan 2026 11:24:36 -0500 Subject: [PATCH 21/30] hardware: Reorganize options structure This is the final part of a change set that mirrors a commit from Julian. This one renames the options from platform to hardware devices, better future-proofing the options. This also has the benefit of not re-using a term (platform) that has an existing meaning with Nixpkgs things (`stdenv.hostPlatform.system`), and should make it less confusing to discuss about. Co-authored-by: Julian Stecklina Signed-off-by: Samuel Dionne-Riel --- modules/hardware/devices/default.nix | 63 ++++++++++++---------------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/modules/hardware/devices/default.nix b/modules/hardware/devices/default.nix index 79a0f89..9edd234 100644 --- a/modules/hardware/devices/default.nix +++ b/modules/hardware/devices/default.nix @@ -1,50 +1,41 @@ { config, lib, ... }@moduleArgs: let - platform = config.ctrl-os.platform; - inherit - (import ../../../lib { inherit lib; }) + cfg = config.ctrl-os.hardware; + inherit (import ../../../lib { inherit lib; }) getVendorsModules ; deviceModules = getVendorsModules ./.; devices = builtins.attrNames deviceModules; in { - options.ctrl-os.platform = lib.mkOption { + options.ctrl-os.hardware.device = lib.mkOption { type = with lib.types; nullOr (enum devices); - description = "The platform, we are running on."; + description = "Selects a hardware device profile to use by device name."; default = null; }; - imports = - builtins.attrValues - ( - builtins.mapAttrs - ( - device: dir: - let - cfgOrFn = import dir; - appliedConfig = - let config = cfgOrFn moduleArgs; in - if config ? config then - config - else - { inherit config; } - ; - cond = lib.mkIf (platform == device); - in - if builtins.isAttrs cfgOrFn then - { - config = cond ( - cfgOrFn - ); - } - else - appliedConfig // { - config = cond appliedConfig.config; - } - ) - deviceModules - ) - ; + imports = builtins.attrValues ( + builtins.mapAttrs ( + device: dir: + let + cfgOrFn = import dir; + appliedConfig = + let + config = cfgOrFn moduleArgs; + in + if config ? config then config else { inherit config; }; + cond = lib.mkIf (cfg.device == device); + in + if builtins.isAttrs cfgOrFn then + { + config = cond (cfgOrFn); + } + else + appliedConfig + // { + config = cond appliedConfig.config; + } + ) deviceModules + ); } From 6f8efa9c8d1071c2994fa8869fc77ba42066b381 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Sun, 25 Jan 2026 17:24:20 -0500 Subject: [PATCH 22/30] jetsonOrinNanoInstaller: Move to new option structure --- packages/flakeModule.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/flakeModule.nix b/packages/flakeModule.nix index aa4c945..9685532 100644 --- a/packages/flakeModule.nix +++ b/packages/flakeModule.nix @@ -34,12 +34,12 @@ { imports = [ "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" - self.nixosModules.platform + self.nixosModules.hardware self.nixosModules.profiles ]; ctrl-os.profiles.developer.enable = true; - ctrl-os.platform = "nvidia-jetson-orin-nano-super"; + ctrl-os.hardware.device = "nvidia-jetson-orin-nano-super"; nixpkgs.hostPlatform = "aarch64-linux"; nixpkgs.buildPlatform = system; system.stateVersion = "25.11"; From 85e4f760e0bd534fad7733cfdb80b3f670f263b2 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Thu, 22 Jan 2026 14:08:28 -0500 Subject: [PATCH 23/30] WIP: packages/hardware: Init hardware device installer outputs This is WIP because this is currently using the `legacyPackages` Flake output attribute, but I believe this is the wrong thing to do. These are not packages, they would not be added to a user's system. The `flakeModule` design seems to break down once you need something that isn't in the "well-known"[sic] outputs of Flakes. --- packages/flakeModule.nix | 5 + packages/hardware/default.nix | 166 ++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 packages/hardware/default.nix diff --git a/packages/flakeModule.nix b/packages/flakeModule.nix index 9685532..4794802 100644 --- a/packages/flakeModule.nix +++ b/packages/flakeModule.nix @@ -25,6 +25,11 @@ perSystem = { pkgs, system, ... }: { + legacyPackages = { + hardware = import ./hardware { + inherit pkgs self; + }; + }; packages = (import ./default.nix { inherit pkgs; }) // { jetsonOrinNanoInstaller = (inputs.nixpkgs.lib.nixosSystem { diff --git a/packages/hardware/default.nix b/packages/hardware/default.nix new file mode 100644 index 0000000..2fe0285 --- /dev/null +++ b/packages/hardware/default.nix @@ -0,0 +1,166 @@ +{ self, pkgs }: + +let + inherit (pkgs) lib; + inherit (pkgs.stdenv.hostPlatform) system; + + # Evaluate a NixOS configuration without relying on the Flakes entrypoint. + evalConfig = + { + system ? null, + ... + }@config: + (import (pkgs.path + "/nixos/lib/eval-config.nix")) ( + config + // { + inherit system; + } + ); + + # Borrow an arbitrary NixOS eval for evaluating the final `options` with + # our `hardware` module imported. + inherit + (evalConfig { + modules = [ self.nixosModules.hardware ]; + # The system does not matter, we only need to evaluate up to the options. + inherit (pkgs.stdenv.hostPlatform) system; + }) + options + ; + + devices = + # Get the device profile option + options.ctrl-os.hardware.device + # Unwrap the nullOr + .type.functor.payload + # Dig into the `enum` + .elemType.functor.payload + # And get the values + .values; + + # Evaluate the CTRL-OS device modules for the given `device`. + # This returns the output from the `output` attribute path, with the evaluation + # merged into the attribute set. + # By default, `system.build.toplevel` is returned, which is the "system build". + evaluate = + device: + { + modules ? [ ], + config ? { }, + output ? [ + "system" + "build" + "toplevel" + ], + }: + + let + eval = evalConfig { + modules = modules ++ [ + ( + { config, ... }: + { + imports = [ + self.nixosModules.hardware + self.nixosModules.profiles + ]; + + # Enable the device-specific config. + ctrl-os.hardware.device = device; + ctrl-os.profiles.ctrl-os-system.enable = true; + + # Ensure this system build will use cross-compilation if relevant... + nixpkgs.buildPlatform = system; + # ... and tag the system build as such. + system.nixos.tags = [ + ( + if config.nixpkgs.hostPlatform.system == config.nixpkgs.buildPlatform.system then + "native" + else + "cross-from-${config.nixpkgs.buildPlatform.system}" + ) + ]; + } + ) + config + ]; + }; + in + (lib.getAttrFromPath output eval.config) // { inherit eval; }; + + # For a given `device`, evaluate the installer config from `path`, + # relative to the nixpkgs `nixos/modules/installer` path. + # The output is the (guessed) relevant build output. + # As with `evaluate`, the system config is added to the derivation + # attribute set as the `eval` attribute. + evaluateInstaller = + device: path: + let + installer = evaluate device { + config = + { modulesPath, ... }: + { + imports = [ + "${modulesPath}/installer/${path}" + ]; + + # This is only safe to do when generating images!!! + # `stateVersion` should not otherwise be set by an imported modules in a user's config. + system.stateVersion = pkgs.lib.trivial.release; + + # Make the installer generate the necessary configuration bits. + # FIXME: this is incomplete right now as the whole installer tooling doesn't know about + # adding CTRL-OS modules, and has no way to do that in any generic fashion. + system.nixos-generate-config.desktopConfiguration = + let + # Escape string values using JSON, as they are mostly compatible with Nix strings. + e = builtins.toJSON; + in + [ + '' + # + # WARNING: Using `nixos-generate-config` with CTRL-OS hardware modules is experimental. + # + # This generated configuration DOES NOT yet include the CTRL-OS configuration for the platform. + # You will first need to handle importing the CTRL-OS modules in your configuration. + # + /* + imports = [ + ctrl-os.nixosModules.hardware + ctrl-os.nixosModules.profiles + ]; + + ctrl-os.hardware.device = ${e device}; + ctrl-os.profiles.ctrl-os-system.enable = true; + + */ + '' + ]; + }; + }; + inherit (installer.eval.config.system) build; + in + (build.isoImage or build.sdImage + or (builtins.throw "Unable to guess the artifact type for installer path ${builtins.toJSON path}") + ) + // { + inherit (installer) eval; + }; +in +{ + devices.installers = lib.listToAttrs ( + lib.map ( + device: + let + mkInstaller = evaluateInstaller device; + in + { + name = device; + value = { + iso = mkInstaller "cd-dvd/installation-cd-minimal.nix"; + sd-image-new-kernel = mkInstaller "sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix"; + }; + } + ) devices + ); +} From 09830c2275772b4f14707e5cfe9f9836519401a0 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Fri, 23 Jan 2026 11:17:05 -0500 Subject: [PATCH 24/30] flakeModule: Drop now-unneeded installer "packages" Signed-off-by: Samuel Dionne-Riel --- packages/flakeModule.nix | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/packages/flakeModule.nix b/packages/flakeModule.nix index 4794802..1cdf80e 100644 --- a/packages/flakeModule.nix +++ b/packages/flakeModule.nix @@ -23,35 +23,13 @@ }; perSystem = - { pkgs, system, ... }: + { pkgs, ... }: { legacyPackages = { hardware = import ./hardware { inherit pkgs self; }; }; - packages = (import ./default.nix { inherit pkgs; }) // { - jetsonOrinNanoInstaller = - (inputs.nixpkgs.lib.nixosSystem { - modules = [ - ( - { modulesPath, ... }: - { - imports = [ - "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" - self.nixosModules.hardware - self.nixosModules.profiles - ]; - - ctrl-os.profiles.developer.enable = true; - ctrl-os.hardware.device = "nvidia-jetson-orin-nano-super"; - nixpkgs.hostPlatform = "aarch64-linux"; - nixpkgs.buildPlatform = system; - system.stateVersion = "25.11"; - } - ) - ]; - }).config.system.build.isoImage; - }; + packages = import ./default.nix { inherit pkgs; }; }; } From 5943a00db5f75b627ca14b8a190308cd8630d4aa Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Mon, 26 Jan 2026 10:32:07 -0500 Subject: [PATCH 25/30] modules: Init hardware.developer namespace These modules are for the *developer systems*, not for the target systems. They are meant to hold either necessary or helpful system-wide configuration for working with target devices. Noting that we should prefer *not* installing system-wide packages for tools and such by default, and mainly ensure that the system-wide configuration is proper. The rationale being that the exact tooling should be part of a developer shell for the target devices, rather than installed system-wide. Especially true when some targets may require tooling of older/newer versions than others, and may need to be worked with concurrently by the developers! --- modules/hardware/default.nix | 1 + modules/hardware/developer/default.nix | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 modules/hardware/developer/default.nix diff --git a/modules/hardware/default.nix b/modules/hardware/default.nix index 605f052..18df388 100644 --- a/modules/hardware/default.nix +++ b/modules/hardware/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./developer ./devices ]; } diff --git a/modules/hardware/developer/default.nix b/modules/hardware/developer/default.nix new file mode 100644 index 0000000..87371fe --- /dev/null +++ b/modules/hardware/developer/default.nix @@ -0,0 +1,11 @@ +{ lib, ... }: + +let + inherit (import ../../../lib { inherit lib; }) + getVendorsModules + ; + developerModules = getVendorsModules ./.; +in +{ + imports = builtins.attrValues developerModules; +} From a478111f63f4cdc1faf00e3030f7eb496994f746 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Mon, 26 Jan 2026 10:36:16 -0500 Subject: [PATCH 26/30] modules: developer.nvidia.tegra: init Add a module that, for now, only adds the udev rules for one Tegra device (Orin). We could guess at other device identifiers by carefully searching online, but that would be untested. This udev rule was verified to work with the platform firmware flashing tooling from this repo. No root privileges required! --- .../developer/nvidia/tegra/default.nix | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 modules/hardware/developer/nvidia/tegra/default.nix diff --git a/modules/hardware/developer/nvidia/tegra/default.nix b/modules/hardware/developer/nvidia/tegra/default.nix new file mode 100644 index 0000000..1eb3986 --- /dev/null +++ b/modules/hardware/developer/nvidia/tegra/default.nix @@ -0,0 +1,49 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.ctrl-os.hardware.developer.nvidia.tegra; + inherit (lib) + mkEnableOption + mkIf + mkMerge + ; + + # Makes an "enable" option that defaults to the `cfg.enable` state. + mkDefaultEnable = + description: + (lib.mkEnableOption description) + // { + default = cfg.enable; + defaultText = "config.ctrl-os.hardware.developer.nvidia.tegra.enable"; + }; +in +{ + options.ctrl-os.hardware.developer.nvidia.tegra = { + enable = mkEnableOption "configuration on a host system for working with Tegra devices"; + enableUdevRules = mkDefaultEnable "udev rules to communicate with the USB download mode for recovery"; + }; + + config = mkMerge [ + (mkIf cfg.enableUdevRules { + services.udev.packages = lib.singleton ( + pkgs.writeTextFile rec { + name = "usb-nvidia-hw.rules"; + text = '' + SUBSYSTEM!="usb", GOTO="end_rules" + + # Orin + ATTRS{idVendor}=="0955", ATTRS{idProduct}=="7523", TAG+="uaccess" + + LABEL="end_rules" + ''; + destination = "/etc/udev/rules.d/70-${name}"; + } + ); + }) + ]; +} From 72e643014019bcea69a5bac7430f4ff66e62907c Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Mon, 26 Jan 2026 09:25:15 -0500 Subject: [PATCH 27/30] DO NOT MERGE: packages: temp reformat before discussing structure --- packages/flakeModule.nix | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/flakeModule.nix b/packages/flakeModule.nix index 1cdf80e..58935c4 100644 --- a/packages/flakeModule.nix +++ b/packages/flakeModule.nix @@ -26,9 +26,10 @@ { pkgs, ... }: { legacyPackages = { - hardware = import ./hardware { - inherit pkgs self; - }; + hardware = + # XXX Installer packages, TBD + (import ./hardware { inherit pkgs self; }) + ; }; packages = import ./default.nix { inherit pkgs; }; }; From 48da35d434978604941f9915832aaff1d151076f Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Mon, 26 Jan 2026 09:29:18 -0500 Subject: [PATCH 28/30] DO NOT MERGE: packages: Add hardware packages scope It's empty. It's getting contents in the follow-up commits. --- packages/flakeModule.nix | 3 ++- packages/hardware/packages.nix | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 packages/hardware/packages.nix diff --git a/packages/flakeModule.nix b/packages/flakeModule.nix index 58935c4..99ec710 100644 --- a/packages/flakeModule.nix +++ b/packages/flakeModule.nix @@ -29,7 +29,8 @@ hardware = # XXX Installer packages, TBD (import ./hardware { inherit pkgs self; }) - ; + # XXX Actual packages, structure TBD too + // (pkgs.callPackage ./hardware/packages.nix { }); }; packages = import ./default.nix { inherit pkgs; }; }; diff --git a/packages/hardware/packages.nix b/packages/hardware/packages.nix new file mode 100644 index 0000000..71f7a05 --- /dev/null +++ b/packages/hardware/packages.nix @@ -0,0 +1,17 @@ +{ + pkgs, + lib, + ... +}: + +lib.makeScope pkgs.newScope ( + self: + let + inherit (self) + callPackage + ; + in + # XXX package structure TBD + { + } +) From feb9ccb0cd92a10d17506f833cbe7d76b9124238 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Mon, 26 Jan 2026 09:30:39 -0500 Subject: [PATCH 29/30] WIP: packages: jetpack-sdks: Init This adds the Jetpack SDKs expression, maybe a bit over-engineered, but this should be an implementation detail for the follow-up steps. The licensing information should be fixed-up so it is proper for the SDK terms. --- packages/hardware/nvidia/tegra/default.nix | 8 +++++++ .../hardware/nvidia/tegra/jetpack-sdks.nix | 23 +++++++++++++++++++ packages/hardware/packages.nix | 1 + 3 files changed, 32 insertions(+) create mode 100644 packages/hardware/nvidia/tegra/default.nix create mode 100644 packages/hardware/nvidia/tegra/jetpack-sdks.nix diff --git a/packages/hardware/nvidia/tegra/default.nix b/packages/hardware/nvidia/tegra/default.nix new file mode 100644 index 0000000..3276c99 --- /dev/null +++ b/packages/hardware/nvidia/tegra/default.nix @@ -0,0 +1,8 @@ +{ + callPackage, + nvidia, +}: + +{ + jetpack-sdks = callPackage ./jetpack-sdks.nix {}; +} diff --git a/packages/hardware/nvidia/tegra/jetpack-sdks.nix b/packages/hardware/nvidia/tegra/jetpack-sdks.nix new file mode 100644 index 0000000..4840890 --- /dev/null +++ b/packages/hardware/nvidia/tegra/jetpack-sdks.nix @@ -0,0 +1,23 @@ +# The compatibility matrix can be found here: +# - https://developer.nvidia.com/embedded/jetson-linux-archive +{ + lib, + fetchurl, +}: + +rec { + versions = { + "36.4.4" = fetchurl { + version = "36.4.4"; + url = "https://developer.nvidia.com/downloads/embedded/l4t/r36_release_v4.4/release/Jetson_Linux_r36.4.4_aarch64.tbz2"; + hash = "sha256-ps4RwiEAqwl25BmVkYJBfIPWL0JyUBvIcU8uB24BDzs="; + meta.nvidiaDriverLicenseAgreement = "https://developer.download.nvidia.com/embedded/L4T/r36_Release_v4.4/release/Tegra_Software_License_Agreement-Tegra-Linux.txt"; + meta.license = [ lib.licenses.unfree ]; + }; + }; + + default = { + # NOTE: Version 38.* currently does not support Jetson Orin systems. + orin = versions."36.4.4"; + }; +} diff --git a/packages/hardware/packages.nix b/packages/hardware/packages.nix index 71f7a05..af5b347 100644 --- a/packages/hardware/packages.nix +++ b/packages/hardware/packages.nix @@ -13,5 +13,6 @@ lib.makeScope pkgs.newScope ( in # XXX package structure TBD { + nvidia.tegra = callPackage ./nvidia/tegra { }; } ) From 8e9fbb76b4ac204c11f37b583396a0c620bb7862 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Mon, 26 Jan 2026 09:35:22 -0500 Subject: [PATCH 30/30] packages: orin.platform-firmware-tooling: Init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a wrapper around the “bootloader”[sic] tooling that simplifies the re-installation of the platform firmware onto a Tegra system. Reflashing a system for the targets part of the vendor SDK should be as simple as calling the wrapper with the appropriate target name. ``` reflash-bios $target ``` The `reflash-bios` script will list the available targets when called without an argument. That is, assuming that proper access is given so the USB device can be communicated with, either through udev rules, or via superuser privileges. The script mainly focuses on dropping anything related to flashing the operating system, and flashing ***only*** the platform firmware. Doing so enables the script to run without root privileges (the scripts are kinda scary and try to do a lot of image manipulation). With Flakes, the tool can be (currently) launched using: ``` export NIXPKGS_ALLOW_UNFREE=1 nix run --impure .#hardware.nvidia.tegra.developer-tools.orin.platform-firmware-tooling ``` --- packages/hardware/nvidia/tegra/default.nix | 10 +- .../platform-firmware-tooling/default.nix | 128 ++++++++++++++++++ .../platform-firmware-tooling/reflash-bios.sh | 41 ++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 packages/hardware/nvidia/tegra/platform-firmware-tooling/default.nix create mode 100755 packages/hardware/nvidia/tegra/platform-firmware-tooling/reflash-bios.sh diff --git a/packages/hardware/nvidia/tegra/default.nix b/packages/hardware/nvidia/tegra/default.nix index 3276c99..a0bc8ba 100644 --- a/packages/hardware/nvidia/tegra/default.nix +++ b/packages/hardware/nvidia/tegra/default.nix @@ -4,5 +4,13 @@ }: { - jetpack-sdks = callPackage ./jetpack-sdks.nix {}; + jetpack-sdks = callPackage ./jetpack-sdks.nix { }; + + developer-tools = { + orin = { + platform-firmware-tooling = callPackage ./platform-firmware-tooling { + sdk = nvidia.tegra.jetpack-sdks.default.orin; + }; + }; + }; } diff --git a/packages/hardware/nvidia/tegra/platform-firmware-tooling/default.nix b/packages/hardware/nvidia/tegra/platform-firmware-tooling/default.nix new file mode 100644 index 0000000..5d5632b --- /dev/null +++ b/packages/hardware/nvidia/tegra/platform-firmware-tooling/default.nix @@ -0,0 +1,128 @@ +{ + lib, + stdenv, + runtimeShell, + writeShellScript, + libarchive, + + coreutils, + dtc, + gcc, # for the C pre-processor, for dtc... + gnused, + python3, + libxml2, # for xmllint + + # Path to the SDK *archive*. + sdk, +}: + +let + path = lib.makeBinPath [ + (python3.withPackages ( + pp: with pp; [ + pyyaml + ] + )) + coreutils + dtc + gcc + gnused + libxml2 + ]; + + # This wrapper is used to make the NVIDIA tooling happier. + # It *really really* wants to have write-access to the tooling's data folders. + # So let's make up a fresh new folder every time. + wrapper = writeShellScript "nvidia-host-pc-tool-wrapper" '' + set -e + set -u + export PS4=" $ " + + export PATH=${lib.escapeShellArg path}:"$PATH" + + sdk="$(readlink -f "''${BASH_SOURCE[0]%/*}")" + original_pwd="$PWD" + dir="$(mktemp -p "''${TMPDIR:-}" -d "nvidia-flashing.XXXXXXXXXX")" + + echo "Going to temp runtime dir: '$dir'" + cd "$dir" + cp --no-preserve=ownership -r "$sdk"/* ./ + chmod -R a+wr . + + ( + set -x + "$@" + ) || : + + ret=$? + + echo "Cleaning-up..." + rm -rf "$dir" + + exit $ret + ''; +in +stdenv.mkDerivation (finalAttrs: { + pname = "nvidia-host-platform-firmware-tooling"; + version = finalAttrs.sdk.version; + + inherit sdk; + + nativeBuildInputs = [ + libarchive + ]; + + # Handle unpacking ourselves, the SDK is unwieldy. + dontUnpack = true; + + installPhase = '' + patterns=( + "Linux_for_Tegra/bootloader" + "Linux_for_Tegra/kernel/dtb" + "Linux_for_Tegra/*.sh" + "Linux_for_Tegra/*.cfg" + "Linux_for_Tegra/*.conf" + "Linux_for_Tegra/*.conf.common" + ) + mkdir -vp $out/bin + + # NOTE: We are expanding '$out' in the script by design. + cat > $out/bin/flash <" + ( + cd @out@/sdk + printf " - %s\n" *.conf | sed -e 's/.conf$//' | sort -u + ) + ) >&2 + exit 1 +fi + +board="$1" +shift + +export FLASHLIGHT=1 # Undocumented flag, used to skip checking for some (effectively unused) operating system files +export NO_ESP_IMG=1 # [undocumented] related to building an ESP filesystem for OS flashing +export NO_RECOVERY_IMG=1 # NO_RECOVERY_IMG -------- Do not create or re-create recovery.img +export NO_ROOTFS=1 # NO_ROOTFS -------------- Do not create or re-create system.img + +args=( + --qspi-only # Flash QSPI device only + --no-root-check # Don't check for root (when usb device access is given through uaccess tag) + --no-systemimg # Do not create or re-create system.img + + # The board and target device arguments need to be last. + # This allows passing more arguments like `-G`, `-k` or `--image`. + "$@" + + # The target board. + "$board" + + # Operating system flashing location, actually unused here. + internal +) + +exec "@out@/bin/flash" "${args[@]}"