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: | diff --git a/README.md b/README.md index 0b74453..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 @@ -36,7 +35,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 diff --git a/checks/default.nix b/checks/default.nix index 9f4f691..4535f29 100644 --- a/checks/default.nix +++ b/checks/default.nix @@ -6,9 +6,17 @@ 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 - (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; }; }; 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 = '' 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; } +) 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": { 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/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 + ; +} 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 diff --git a/modules/default.nix b/modules/default.nix index 6593465..8f272ac 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,7 +1,5 @@ -lib: { - developer = import ./developer.nix; -} -// lib.optionalAttrs (lib.versionAtLeast lib.version "25.11") { + 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..18df388 --- /dev/null +++ b/modules/hardware/default.nix @@ -0,0 +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; +} 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}"; + } + ); + }) + ]; +} diff --git a/modules/hardware/devices/default.nix b/modules/hardware/devices/default.nix new file mode 100644 index 0000000..9edd234 --- /dev/null +++ b/modules/hardware/devices/default.nix @@ -0,0 +1,41 @@ +{ config, lib, ... }@moduleArgs: + +let + cfg = config.ctrl-os.hardware; + inherit (import ../../../lib { inherit lib; }) + getVendorsModules + ; + deviceModules = getVendorsModules ./.; + devices = builtins.attrNames deviceModules; +in +{ + options.ctrl-os.hardware.device = lib.mkOption { + type = with lib.types; nullOr (enum devices); + 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 (cfg.device == device); + in + if builtins.isAttrs cfgOrFn then + { + config = cond (cfgOrFn); + } + else + appliedConfig + // { + config = cond appliedConfig.config; + } + ) deviceModules + ); +} diff --git a/modules/hardware/devices/nvidia/jetson-orin-nano-super/default.nix b/modules/hardware/devices/nvidia/jetson-orin-nano-super/default.nix new file mode 100644 index 0000000..a6b0412 --- /dev/null +++ b/modules/hardware/devices/nvidia/jetson-orin-nano-super/default.nix @@ -0,0 +1,12 @@ +{ + 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" + ]; +} 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 { 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; + }) + ]; +} diff --git a/packages/flakeModule.nix b/packages/flakeModule.nix index 64ed189..99ec710 100644 --- a/packages/flakeModule.nix +++ b/packages/flakeModule.nix @@ -1,4 +1,9 @@ -{ withSystem, inputs, ... }: +{ + withSystem, + inputs, + self, + ... +}: { } // inputs.nixpkgs.lib.optionalAttrs @@ -20,6 +25,13 @@ perSystem = { pkgs, ... }: { + legacyPackages = { + 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/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 + ); +} diff --git a/packages/hardware/nvidia/tegra/default.nix b/packages/hardware/nvidia/tegra/default.nix new file mode 100644 index 0000000..a0bc8ba --- /dev/null +++ b/packages/hardware/nvidia/tegra/default.nix @@ -0,0 +1,16 @@ +{ + callPackage, + nvidia, +}: + +{ + 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/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/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[@]}" diff --git a/packages/hardware/packages.nix b/packages/hardware/packages.nix new file mode 100644 index 0000000..af5b347 --- /dev/null +++ b/packages/hardware/packages.nix @@ -0,0 +1,18 @@ +{ + pkgs, + lib, + ... +}: + +lib.makeScope pkgs.newScope ( + self: + let + inherit (self) + callPackage + ; + in + # XXX package structure TBD + { + nvidia.tegra = callPackage ./nvidia/tegra { }; + } +)