├── docs ├── content │ ├── stylesheets │ │ └── extra.css │ ├── reference │ │ └── .gitignore │ ├── index.md │ ├── assets │ │ └── images │ │ │ ├── logo.png │ │ │ └── hero.svg │ ├── contributing │ │ ├── code.md │ │ └── docs.md │ └── getting-started │ │ └── generate-report.md └── theme │ └── home.html ├── nix ├── package.nix ├── packages │ └── nixos-facter │ │ ├── goVendorHash.txt │ │ ├── default.nix │ │ ├── tests │ │ ├── disko.nix │ │ └── default.nix │ │ └── package.nix ├── devshells │ ├── docs.nix │ └── default.nix └── formatter.nix ├── .gitignore ├── main.go ├── renovate.json ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yaml │ ├── update-vendor-hash.sh │ ├── update-flake-inputs.yml │ ├── update-vendor-hash.yml │ └── gh-pages.yml ├── .envrc ├── .mergify.yml ├── go.mod ├── .golangci.yml ├── pkg ├── hwinfo │ ├── smbios_config.go │ ├── smbios_oem_strings.go │ ├── smbios_language.go │ ├── smbios_any.go │ ├── smbios_pointing_device.go │ ├── driver_info_dsl.go │ ├── smbios_group_associations.go │ ├── virtual_network_devices.go │ ├── driver_info_isdn.go │ ├── resource_cache.go │ ├── smbios_power_controls.go │ ├── smbios_hardware_security.go │ ├── resource_phys_mem.go │ ├── resource_pppd_option.go │ ├── driver_info_keyboard.go │ ├── detail_sys.go │ ├── driver_info_mouse.go │ ├── resource_init_strings.go │ ├── smbios_bios.go │ ├── resource_hw_addr.go │ ├── smbios_slot.go │ ├── smbios_sys_info.go │ ├── resource_link.go │ ├── resource_dma.go │ ├── smbios_memory_array_mapped_address.go │ ├── resource_baud.go │ ├── resource_irq.go │ ├── smbios_port_connector.go │ ├── smbios_memory_array.go │ ├── resource_frame_buffer.go │ ├── resource_wlan.go │ ├── smbios_onboard.go │ ├── resource_io.go │ ├── resource_size.go │ ├── iommu.go │ ├── resource_monitor.go │ ├── smbios_board.go │ ├── resource_disk_geo.go │ ├── smbios_memory_error.go │ ├── smbios_memory64_error.go │ ├── utils.go │ ├── driver_info_module.go │ ├── smbios_memory_device_mapped_address.go │ ├── smbios_chassis.go │ ├── resource_fc.go │ ├── driver_info_display.go │ ├── smbios_cache.go │ ├── detail_bios.go │ ├── resource_mem.go │ ├── id.go │ ├── driver_info.go │ ├── smbios_memory_device.go │ ├── smbios_processor.go │ ├── detail_isa_pnp_dev.go │ ├── hwinfo.go │ ├── detail_monitor.go │ ├── detail_enum_pci_flag.go │ ├── driver_info_x11.go │ ├── detail.go │ ├── resource_enum_yes_no_flags.go │ ├── hardware_enum_sub_class_keyboard.go │ ├── resource_enum_geo_type.go │ ├── id_tag_enum.go │ ├── hardware_enum_hotplug.go │ ├── resource_enum_access_flags.go │ ├── resource_enum_size_unit.go │ ├── hardware_enum_sub_class_mouse.go │ ├── resource.go │ ├── detail_pci.go │ ├── detail_usb.go │ ├── input.go │ └── driver_info_enum_type.go ├── build │ └── build.go ├── boot │ └── boot.go ├── virt │ └── virt.go ├── ephem │ ├── ephem.go │ ├── swap_test.go │ ├── swap_enum_type.go │ └── swap.go └── linux │ └── input │ └── input_test.go ├── LICENSE ├── flake.nix ├── go.sum ├── scripts └── create-release.sh ├── mkdocs.yml ├── README.md └── flake.lock /docs/content/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/content/reference/.gitignore: -------------------------------------------------------------------------------- 1 | go_doc -------------------------------------------------------------------------------- /nix/package.nix: -------------------------------------------------------------------------------- 1 | { perSystem, ... }: perSystem.self.nixos-facter 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | result* 3 | *.qcow2 4 | .data 5 | report.json 6 | .cache 7 | site -------------------------------------------------------------------------------- /nix/packages/nixos-facter/goVendorHash.txt: -------------------------------------------------------------------------------- 1 | sha256-5duwAxAgbPZIbbgzZE2m574TF/0+jF/TvTKI4YBH6jM= 2 | -------------------------------------------------------------------------------- /docs/content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: home.html 3 | search: 4 | exclude: true 5 | --- 6 | 7 | # Home 8 | -------------------------------------------------------------------------------- /docs/content/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/nixos-facter/HEAD/docs/content/assets/images/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/numtide/nixos-facter/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /nix/devshells/docs.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | ... 4 | }: 5 | pkgs.mkShellNoCC { 6 | packages = with pkgs.python3Packages; [ 7 | mike 8 | mkdocs 9 | mkdocs-material 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "lockFileMaintenance": { "enabled": true }, 5 | "nix": { 6 | "enabled": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /nix/packages/nixos-facter/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs, 3 | perSystem, 4 | pkgs, 5 | ... 6 | }: 7 | pkgs.callPackage ./package.nix { 8 | hwinfo = perSystem.hwinfo.default; 9 | } 10 | // { 11 | passthru.tests = import ./tests { 12 | inherit pkgs inputs perSystem; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | if ! has nix_direnv_version || ! nix_direnv_version 3.0.5; then 2 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.5/direnvrc" "sha256-RuwIS+QKFj/T9M2TFXScjBsLR6V3A17YVoEW/Q6AZ1w=" 3 | fi 4 | 5 | watch_file flake.nix 6 | watch_file nix/devshells/default.nix 7 | 8 | use flake -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | merge_conditions: 4 | - check-success=buildbot/nix-eval 5 | merge_method: rebase 6 | 7 | pull_request_rules: 8 | - name: refactored queue action rule 9 | conditions: 10 | - base=main 11 | - label~=merge-queue|dependencies 12 | actions: 13 | queue: 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/numtide/nixos-facter 2 | 3 | go 1.24.1 4 | 5 | require github.com/stretchr/testify v1.11.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 9 | github.com/kr/pretty v0.3.1 // indirect 10 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 11 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 12 | gopkg.in/yaml.v3 v3.0.1 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yaml: -------------------------------------------------------------------------------- 1 | name: Auto Merge Dependency Updates 2 | on: 3 | - pull_request_target 4 | jobs: 5 | auto-merge-dependency-updates: 6 | runs-on: ubuntu-latest 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | concurrency: 11 | group: "auto-merge:${{ github.head_ref }}" 12 | cancel-in-progress: true 13 | steps: 14 | - uses: Mic92/auto-merge@main 15 | -------------------------------------------------------------------------------- /nix/devshells/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | perSystem, 3 | pkgs, 4 | ... 5 | }: 6 | perSystem.self.nixos-facter.overrideAttrs (old: { 7 | GOROOT = "${old.passthru.go}/share/go"; 8 | nativeBuildInputs = old.nativeBuildInputs ++ [ 9 | pkgs.enumer 10 | pkgs.delve 11 | pkgs.gotools 12 | pkgs.golangci-lint 13 | pkgs.cobra-cli 14 | pkgs.fx # json tui 15 | perSystem.hwinfo.default 16 | ]; 17 | shellHook = '' 18 | # this is only needed for hermetic builds 19 | unset GO_NO_VENDOR_CHECKS GOSUMDB GOPROXY GOFLAGS 20 | ''; 21 | }) 22 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | default: all 5 | disable: 6 | - cyclop 7 | - err113 8 | - depguard 9 | - exhaustruct 10 | - funlen 11 | - godox 12 | - godot 13 | - mnd 14 | - varnamelen 15 | - forbidigo 16 | - tagliatelle 17 | - nlreturn 18 | - gochecknoglobals 19 | 20 | settings: 21 | cyclop: 22 | max-complexity: 15 23 | revive: 24 | rules: 25 | - name: "exported" 26 | disabled: true 27 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_config.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosConfig captures system config information. 10 | type SmbiosConfig struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | Options []string `json:"options,omitempty"` 14 | } 15 | 16 | func (s SmbiosConfig) SmbiosType() SmbiosType { 17 | return s.Type 18 | } 19 | 20 | func NewSmbiosConfig(info C.smbios_config_t) (*SmbiosConfig, error) { 21 | return &SmbiosConfig{ 22 | Type: SmbiosTypeConfig, 23 | Handle: int(info.handle), 24 | Options: ReadStringList(info.options), 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/build/build.go: -------------------------------------------------------------------------------- 1 | // Package build contains constants and values set at build time via -X flags. 2 | package build 3 | 4 | var ( 5 | // Name is the program name, typically set via Nix to match the derivation's `pname`. 6 | Name = "nixos-facter" 7 | // Version is the program version, typically set via Nix to match the derivation's `version`. 8 | Version = "v0.0.0+dev" 9 | // System is the architecture that this program was built for e.g. x86_64-linux. 10 | // It is set via Nix to match the Nixpkgs system. 11 | System = "" 12 | // ReportVersion is used to indicate significant changes in the report output and is embedded JSON report produced. 13 | ReportVersion uint16 = 1 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_oem_strings.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosOEMStrings captures OEM information. 10 | type SmbiosOEMStrings struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | Strings []string `json:"strings,omitempty"` 14 | } 15 | 16 | func (s SmbiosOEMStrings) SmbiosType() SmbiosType { 17 | return s.Type 18 | } 19 | 20 | func NewSmbiosOEM(info C.smbios_oem_t) (*SmbiosOEMStrings, error) { 21 | return &SmbiosOEMStrings{ 22 | Type: SmbiosTypeOEMStrings, 23 | Handle: int(info.handle), 24 | Strings: ReadStringList(info.oem_strings), 25 | }, nil 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GPL v3 License 2 | 3 | Copyright (C) 2024 Numtide & Contributors 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . -------------------------------------------------------------------------------- /.github/workflows/update-vendor-hash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell -i bash -p nix -p coreutils -p gnused -p gawk 3 | # shellcheck shell=bash 4 | 5 | set -exuo pipefail 6 | 7 | cd "$(git rev-parse --show-toplevel)" 8 | 9 | go mod tidy 10 | 11 | # shellcheck disable=SC2016 12 | failedbuild=$(nix build --log-format bar-with-logs --impure --expr '(builtins.getFlake (toString ./.)).packages.${builtins.currentSystem}.nixos-facter.overrideAttrs (_:{ vendorHash = ""; })' 2>&1 || true) 13 | echo "$failedbuild" 14 | checksum=$(echo "$failedbuild" | awk '/got:.*sha256/ { print $2 }') 15 | 16 | if [ -z "$checksum" ]; then 17 | echo "Error: Could not extract checksum from build output" 18 | exit 1 19 | fi 20 | 21 | echo "$checksum" > nix/packages/nixos-facter/goVendorHash.txt 22 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_language.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosLanguage captures language information. 10 | type SmbiosLanguage struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | Languages []string `json:"languages,omitempty"` 14 | CurrentLanguage string `json:"-"` 15 | } 16 | 17 | func (s SmbiosLanguage) SmbiosType() SmbiosType { 18 | return s.Type 19 | } 20 | 21 | func NewSmbiosLang(info C.smbios_lang_t) (*SmbiosLanguage, error) { 22 | return &SmbiosLanguage{ 23 | Type: SmbiosTypeLanguage, 24 | Handle: int(info.handle), 25 | Languages: ReadStringList(info.strings), 26 | CurrentLanguage: C.GoString(info.current), 27 | }, nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_any.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "fmt" 11 | "unsafe" 12 | ) 13 | 14 | // SmbiosAny captures generic smbios data. 15 | type SmbiosAny struct { 16 | Type SmbiosType `json:"-"` 17 | Handle int `json:"handle"` 18 | Data string `json:"data"` 19 | Strings []string `json:"strings,omitempty"` 20 | } 21 | 22 | func (s SmbiosAny) SmbiosType() SmbiosType { 23 | return s.Type 24 | } 25 | 26 | func NewSmbiosAny(smbiosType SmbiosType, info C.smbios_any_t) (*SmbiosAny, error) { 27 | return &SmbiosAny{ 28 | Type: smbiosType, 29 | Handle: int(info.handle), 30 | Data: fmt.Sprintf("0x%x", ReadByteArray(unsafe.Pointer(info.data), int(info.data_len))), 31 | Strings: ReadStringList(info.strings), 32 | }, nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_pointing_device.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosPointingDevice captures pointing device (aka 'mouse') information. 10 | type SmbiosPointingDevice struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | MouseType *ID `json:"mouse_type"` 14 | Interface *ID `json:"interface"` 15 | Buttons uint16 `json:"buttons"` 16 | } 17 | 18 | func (s SmbiosPointingDevice) SmbiosType() SmbiosType { 19 | return s.Type 20 | } 21 | 22 | func NewSmbiosMouse(info C.smbios_mouse_t) (*SmbiosPointingDevice, error) { 23 | return &SmbiosPointingDevice{ 24 | Type: SmbiosTypePointingDevice, 25 | Handle: int(info.handle), 26 | MouseType: NewID(info.mtype), 27 | Interface: NewID(info._interface), 28 | Buttons: uint16(info.buttons), 29 | }, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/hwinfo/driver_info_dsl.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | type DriverInfoDsl struct { 10 | Type DriverInfoType `json:"type,omitempty"` 11 | // actual driver database entries 12 | DBEntry0 []string `json:"db_entry_0,omitempty"` 13 | DBEntry1 []string `json:"db_entry_1,omitempty"` 14 | 15 | Mode string `json:"mode,omitempty"` // DSL driver types 16 | Name string `json:"name,omitempty"` // DSL driver name 17 | } 18 | 19 | func (d DriverInfoDsl) DriverInfoType() DriverInfoType { 20 | return DriverInfoTypeDsl 21 | } 22 | 23 | func NewDriverInfoDsl(info C.driver_info_dsl_t) DriverInfoDsl { 24 | return DriverInfoDsl{ 25 | Type: DriverInfoTypeDsl, 26 | DBEntry0: ReadStringList(info.hddb0), 27 | DBEntry1: ReadStringList(info.hddb1), 28 | Mode: C.GoString(info.mode), 29 | Name: C.GoString(info.name), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_group_associations.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | import "unsafe" 9 | 10 | // SmbiosGroupAssociations captures group associations. 11 | type SmbiosGroupAssociations struct { 12 | Type SmbiosType `json:"-"` 13 | Handle int `json:"handle"` 14 | Name string `json:"name"` // group name 15 | Handles []int `json:"handles,omitempty"` // array of item handles 16 | } 17 | 18 | func (s SmbiosGroupAssociations) SmbiosType() SmbiosType { 19 | return s.Type 20 | } 21 | 22 | func NewSmbiosGroup(info C.smbios_group_t) (*SmbiosGroupAssociations, error) { 23 | return &SmbiosGroupAssociations{ 24 | Type: SmbiosTypeGroupAssociations, 25 | Handle: int(info.handle), 26 | Name: C.GoString(info.name), 27 | Handles: ReadIntArray(unsafe.Pointer(info.item_handles), int(info.items_len)), 28 | }, nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/hwinfo/virtual_network_devices.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | // original list taken from systemd.network. 4 | var virtualNetworkDevices = map[string]bool{ 5 | "bonding": true, 6 | "bridge": true, 7 | "dummy": true, 8 | "ip_gre": true, 9 | "ip6_gre": true, 10 | "ipip": true, 11 | "ipvlan": true, 12 | "macvlan": true, 13 | "sit": true, 14 | "tun": true, 15 | "veth": true, 16 | "8021q": true, 17 | "ip_vti": true, 18 | "ip6_vti": true, 19 | "vxlan": true, 20 | "geneve": true, 21 | "macsec": true, 22 | "vrf": true, 23 | "vcan": true, 24 | "vxcan": true, 25 | "wireguard": true, 26 | "nlmon": true, 27 | "fou": true, 28 | "ifb": true, 29 | "bareudp": true, 30 | "batman-adv": true, 31 | "ib_ipoib": true, 32 | // FIXME: could not confirm the following netdev drivers: 33 | // "l2tp": true, 34 | // "xfrm": true, 35 | // "erspan": true, 36 | // "ip6tnl": true, 37 | } 38 | -------------------------------------------------------------------------------- /pkg/hwinfo/driver_info_isdn.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | type DriverInfoIsdn struct { 10 | Type DriverInfoType `json:"type,omitempty"` 11 | // actual driver database entries 12 | DBEntry0 []string `json:"db_entry_0,omitempty"` 13 | DBEntry1 []string `json:"db_entry_1,omitempty"` 14 | 15 | I4lType int `json:"i4l_type"` 16 | I4lSubtype int `json:"i4l_sub_type"` 17 | I4lName string `json:"i4l_name,omitempty"` 18 | // todo isdn params 19 | } 20 | 21 | func (d DriverInfoIsdn) DriverInfoType() DriverInfoType { 22 | return DriverInfoTypeIsdn 23 | } 24 | 25 | func NewDriverInfoIsdn(info C.driver_info_isdn_t) DriverInfoIsdn { 26 | return DriverInfoIsdn{ 27 | Type: DriverInfoTypeIsdn, 28 | DBEntry0: ReadStringList(info.hddb0), 29 | DBEntry1: ReadStringList(info.hddb1), 30 | I4lType: int(info.i4l_type), 31 | I4lSubtype: int(info.i4l_subtype), 32 | I4lName: C.GoString(info.i4l_name), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_cache.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_cache_t hd_res_get_cache(hd_res_t *res) { return res->cache; } 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | type ResourceCache struct { 18 | Type ResourceType `json:"type"` 19 | Size string `json:"size"` 20 | } 21 | 22 | func (r ResourceCache) ResourceType() ResourceType { 23 | return ResourceTypeCache 24 | } 25 | 26 | func NewResourceCache(res *C.hd_res_t, resType ResourceType) (*ResourceCache, error) { 27 | if res == nil { 28 | return nil, errors.New("res is nil") 29 | } 30 | 31 | if resType != ResourceTypeCache { 32 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeCache, resType) 33 | } 34 | 35 | cache := C.hd_res_get_cache(res) 36 | 37 | return &ResourceCache{ 38 | Type: resType, 39 | Size: fmt.Sprintf("0x%x", uint(cache.size)), 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_power_controls.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosPowerControls captures system power controls information. 10 | type SmbiosPowerControls struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | Month uint8 `json:"month"` // next scheduled power-on month 14 | Day uint16 `json:"day"` // dto, day 15 | Hour uint8 `json:"hour"` // dto, hour 16 | Minute uint8 `json:"minute"` // dto, minute 17 | Second uint8 `json:"second"` // dto, second 18 | } 19 | 20 | func (s SmbiosPowerControls) SmbiosType() SmbiosType { 21 | return s.Type 22 | } 23 | 24 | func NewSmbiosPower(info C.smbios_power_t) (*SmbiosPowerControls, error) { 25 | return &SmbiosPowerControls{ 26 | Type: SmbiosTypePowerControls, 27 | Handle: int(info.handle), 28 | Month: uint8(info.month), 29 | Day: uint16(info.day), 30 | Hour: uint8(info.hour), 31 | Minute: uint8(info.minute), 32 | Second: uint8(info.second), 33 | }, nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_hardware_security.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosHardwareSecurity captures hardware security information. 10 | type SmbiosHardwareSecurity struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | Power *ID `json:"power"` // power-on password status 14 | Keyboard *ID `json:"keyboard"` // keyboard password status 15 | Admin *ID `json:"admin"` // admin password status 16 | Reset *ID `json:"reset"` // front panel reset status 17 | } 18 | 19 | func (s SmbiosHardwareSecurity) SmbiosType() SmbiosType { 20 | return s.Type 21 | } 22 | 23 | func NewSmbiosSecure(info C.smbios_secure_t) (*SmbiosHardwareSecurity, error) { 24 | return &SmbiosHardwareSecurity{ 25 | Type: SmbiosTypeHardwareSecurity, 26 | Handle: int(info.handle), 27 | Power: NewID(info.power), 28 | Keyboard: NewID(info.keyboard), 29 | Admin: NewID(info.admin), 30 | Reset: NewID(info.reset), 31 | }, nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_phys_mem.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_phys_mem_t hd_res_get_phys_mem(hd_res_t *res) { return res->phys_mem; } 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | type ResourcePhysicalMemory struct { 18 | Type ResourceType `json:"type"` 19 | Range uint64 `json:"range"` 20 | } 21 | 22 | func (r ResourcePhysicalMemory) ResourceType() ResourceType { 23 | return ResourceTypePhysMem 24 | } 25 | 26 | func NewResourcePhysicalMemory(res *C.hd_res_t, resType ResourceType) (*ResourcePhysicalMemory, error) { 27 | if res == nil { 28 | return nil, errors.New("res is nil") 29 | } 30 | 31 | if resType != ResourceTypePhysMem { 32 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypePhysMem, resType) 33 | } 34 | 35 | physMem := C.hd_res_get_phys_mem(res) 36 | 37 | return &ResourcePhysicalMemory{ 38 | Type: resType, 39 | Range: uint64(physMem._range), 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_pppd_option.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_pppd_option_t hd_res_get_pppd_option(hd_res_t *res) { return res->pppd_option; } 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | type ResourcePppdOption struct { 18 | Type ResourceType `json:"type"` 19 | Option byte `json:"option"` 20 | } 21 | 22 | func (r ResourcePppdOption) ResourceType() ResourceType { 23 | return ResourceTypePppdOption 24 | } 25 | 26 | func NewResourcePppdOption(res *C.hd_res_t, resType ResourceType) (*ResourcePppdOption, error) { 27 | if res == nil { 28 | return nil, errors.New("res is nil") 29 | } 30 | 31 | if resType != ResourceTypePppdOption { 32 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypePppdOption, resType) 33 | } 34 | 35 | pppd := C.hd_res_get_pppd_option(res) 36 | 37 | return &ResourcePppdOption{ 38 | Type: resType, 39 | Option: byte(*pppd.option), 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/update-flake-inputs.yml: -------------------------------------------------------------------------------- 1 | name: Update Flake Inputs 2 | on: 3 | schedule: 4 | - cron: "0 2 * * *" 5 | workflow_dispatch: 6 | jobs: 7 | update-flake-inputs: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | steps: 13 | - name: Generate GitHub App Token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: ${{ secrets.APP_ID }} 18 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 19 | - name: Checkout repository 20 | uses: actions/checkout@v6 21 | with: 22 | token: ${{ steps.app-token.outputs.token }} 23 | - name: Setup Nix 24 | uses: cachix/install-nix-action@v31 25 | - name: Update flake inputs 26 | uses: mic92/update-flake-inputs@main 27 | with: 28 | github-token: ${{ steps.app-token.outputs.token }} 29 | auto-merge: true 30 | -------------------------------------------------------------------------------- /pkg/hwinfo/driver_info_keyboard.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | type DriverInfoKeyboard struct { 10 | Type DriverInfoType `json:"type,omitempty"` 11 | // actual driver database entries 12 | DBEntry0 []string `json:"db_entry_0,omitempty"` 13 | DBEntry1 []string `json:"db_entry_1,omitempty"` 14 | 15 | XkbRules string `json:"xkb_rules,omitempty"` 16 | XkbModel string `json:"xkb_model,omitempty"` 17 | XkbLayout string `json:"xkb_layout,omitempty"` 18 | Keymap string `json:"keymap,omitempty"` 19 | } 20 | 21 | func (d DriverInfoKeyboard) DriverInfoType() DriverInfoType { 22 | return DriverInfoTypeKeyboard 23 | } 24 | 25 | func NewDriverInfoKeyboard(info C.driver_info_kbd_t) DriverInfoKeyboard { 26 | return DriverInfoKeyboard{ 27 | Type: DriverInfoTypeKeyboard, 28 | DBEntry0: ReadStringList(info.hddb0), 29 | DBEntry1: ReadStringList(info.hddb1), 30 | XkbRules: C.GoString(info.XkbRules), 31 | XkbModel: C.GoString(info.XkbModel), 32 | XkbLayout: C.GoString(info.XkbLayout), 33 | Keymap: C.GoString(info.keymap), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/hwinfo/detail_sys.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | type DetailSys struct { 10 | Type DetailType `json:"-"` 11 | SystemType string `json:"system_type,omitempty"` 12 | Generation string `json:"generation,omitempty"` 13 | Vendor string `json:"vendor,omitempty"` 14 | Model string `json:"model,omitempty"` 15 | Serial string `json:"-"` 16 | Language string `json:"language,omitempty"` 17 | FormFactor string `json:"form_factor,omitempty"` 18 | } 19 | 20 | func (d DetailSys) DetailType() DetailType { 21 | return DetailTypeSys 22 | } 23 | 24 | func NewDetailSys(sys C.hd_detail_sys_t) (*DetailSys, error) { 25 | data := sys.data 26 | 27 | return &DetailSys{ 28 | Type: DetailTypeSys, 29 | SystemType: C.GoString(data.system_type), 30 | Generation: C.GoString(data.generation), 31 | Vendor: C.GoString(data.vendor), 32 | Model: C.GoString(data.model), 33 | Serial: C.GoString(data.serial), 34 | Language: C.GoString(data.lang), 35 | FormFactor: C.GoString(data.formfactor), 36 | }, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/hwinfo/driver_info_mouse.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | type DriverInfoMouse struct { 10 | Type DriverInfoType `json:"type,omitempty"` 11 | // actual driver database entries 12 | DBEntry0 []string `json:"db_entry_0,omitempty"` 13 | DBEntry1 []string `json:"db_entry_1,omitempty"` 14 | 15 | XF86 string `json:"xf86,omitempty"` // XF86 protocol name 16 | GPM string `json:"gpm,omitempty"` // dto, gpm 17 | Buttons int `json:"buttons,omitempty"` // number of buttons, -1 -> unknown 18 | Wheels int `json:"wheels,omitempty"` // dto, wheels 19 | } 20 | 21 | func (d DriverInfoMouse) DriverInfoType() DriverInfoType { 22 | return DriverInfoTypeMouse 23 | } 24 | 25 | func NewDriverInfoMouse(info C.driver_info_mouse_t) DriverInfoMouse { 26 | return DriverInfoMouse{ 27 | Type: DriverInfoTypeMouse, 28 | DBEntry0: ReadStringList(info.hddb0), 29 | DBEntry1: ReadStringList(info.hddb1), 30 | XF86: C.GoString(info.xf86), 31 | GPM: C.GoString(info.gpm), 32 | Buttons: int(info.buttons), 33 | Wheels: int(info.wheels), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_init_strings.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_init_strings_t hd_res_get_init_strings(hd_res_t *res) { return res->init_strings; } 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | type ResourceInitStrings struct { 18 | Type ResourceType 19 | Init1 string `json:"init_1,omitempty"` 20 | Init2 string `json:"init_2,omitempty"` 21 | } 22 | 23 | func (r ResourceInitStrings) ResourceType() ResourceType { 24 | return ResourceTypeInitStrings 25 | } 26 | 27 | func NewResourceInitStrings(res *C.hd_res_t, resType ResourceType) (*ResourceInitStrings, error) { 28 | if res == nil { 29 | return nil, errors.New("res is nil") 30 | } 31 | 32 | if resType != ResourceTypeInitStrings { 33 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeInitStrings, resType) 34 | } 35 | 36 | init := C.hd_res_get_init_strings(res) 37 | 38 | return &ResourceInitStrings{ 39 | Type: resType, 40 | Init1: C.GoString(init.init1), 41 | Init2: C.GoString(init.init2), 42 | }, nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_bios.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "fmt" 11 | ) 12 | 13 | // SmbiosBios captures BIOS related information. 14 | type SmbiosBios struct { 15 | Type SmbiosType `json:"-"` 16 | Handle int `json:"handle"` 17 | Vendor string `json:"vendor"` 18 | Version string `json:"version"` 19 | Date string `json:"date"` 20 | Features []string `json:"features"` 21 | StartAddress string `json:"start_address"` 22 | RomSize uint32 `json:"rom_size"` 23 | } 24 | 25 | func (s SmbiosBios) SmbiosType() SmbiosType { 26 | return s.Type 27 | } 28 | 29 | func NewSmbiosBiosInfo(info C.smbios_biosinfo_t) (*SmbiosBios, error) { 30 | return &SmbiosBios{ 31 | Type: SmbiosTypeBios, 32 | Handle: int(info.handle), 33 | Vendor: C.GoString(info.vendor), 34 | Version: C.GoString(info.version), 35 | Date: C.GoString(info.date), 36 | Features: ReadStringList(info.feature.str), 37 | StartAddress: fmt.Sprintf("0x%x", uint(info.start)), 38 | RomSize: uint32(info.rom_size), 39 | }, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_hw_addr.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_hwaddr_t hd_res_get_hwaddr(hd_res_t *res) { return res->hwaddr; } 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | type ResourceHardwareAddress struct { 18 | Type ResourceType `json:"type"` 19 | Address byte `json:"address"` 20 | } 21 | 22 | func (r ResourceHardwareAddress) ResourceType() ResourceType { 23 | return r.Type 24 | } 25 | 26 | func NewResourceHardwareAddress(res *C.hd_res_t, resType ResourceType) (*ResourceHardwareAddress, error) { 27 | //nolint:staticcheck 28 | if !(resType == ResourceTypeHwaddr || resType == ResourceTypePhwaddr) { 29 | return nil, fmt.Errorf( 30 | "invalid resource type %s, must be either %s or %s", 31 | resType, ResourceTypeHwaddr, ResourceTypePhwaddr, 32 | ) 33 | } 34 | 35 | if res == nil { 36 | return nil, errors.New("res is nil") 37 | } 38 | 39 | hwaddr := C.hd_res_get_hwaddr(res) 40 | 41 | return &ResourceHardwareAddress{ 42 | Type: resType, 43 | Address: byte(*hwaddr.addr), 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_slot.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosSlot captures system slot information. 10 | type SmbiosSlot struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | Designation string `json:"designation,omitempty"` 14 | SlotType *ID `json:"slot_type"` 15 | BusWidth *ID `json:"bus_width"` 16 | Usage *ID `json:"usage"` 17 | Length *ID `json:"length"` 18 | ID uint16 `json:"id"` 19 | Features []string `json:"features,omitempty"` 20 | } 21 | 22 | func (s SmbiosSlot) SmbiosType() SmbiosType { 23 | return s.Type 24 | } 25 | 26 | func NewSmbiosSlot(info C.smbios_slot_t) (*SmbiosSlot, error) { 27 | return &SmbiosSlot{ 28 | Type: SmbiosTypeSlot, 29 | Handle: int(info.handle), 30 | Designation: C.GoString(info.desig), 31 | SlotType: NewID(info.slot_type), 32 | BusWidth: NewID(info.bus_width), 33 | Usage: NewID(info.usage), 34 | Length: NewID(info.length), 35 | ID: uint16(info.id), 36 | Features: ReadStringList(info.feature.str), 37 | }, nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_sys_info.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosSystem captures overall system related information. 10 | type SmbiosSystem struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | Manufacturer string `json:"manufacturer"` 14 | Product string `json:"product"` 15 | Version string `json:"version"` 16 | Serial string `json:"-"` // omit from json output 17 | UUID string `json:"-"` // universal unique id; all 0x00: undef, all 0xff: undef but settable 18 | WakeUp *ID `json:"wake_up"` // wake-up type 19 | } 20 | 21 | func (s SmbiosSystem) SmbiosType() SmbiosType { 22 | return s.Type 23 | } 24 | 25 | func NewSmbiosSysInfo(info C.smbios_sysinfo_t) (*SmbiosSystem, error) { 26 | return &SmbiosSystem{ 27 | Type: SmbiosTypeSystem, 28 | Handle: int(info.handle), 29 | Manufacturer: C.GoString(info.manuf), 30 | Product: C.GoString(info.product), 31 | Version: C.GoString(info.version), 32 | Serial: C.GoString(info.serial), 33 | // todo uuid 34 | WakeUp: NewID(info.wake_up), 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_link.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool hd_res_link_get_connected(res_link_t res) { return res.state == 1; } 10 | 11 | // CGO cannot access union type fields, so we do this as a workaround 12 | res_link_t hd_res_get_link(hd_res_t *res) { return res->link; } 13 | */ 14 | import "C" 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | ) 20 | 21 | type ResourceLink struct { 22 | Type ResourceType `json:"type"` 23 | Connected bool `json:"connected"` 24 | } 25 | 26 | func (r ResourceLink) ResourceType() ResourceType { 27 | return ResourceTypeLink 28 | } 29 | 30 | func NewResourceLink(res *C.hd_res_t, resType ResourceType) (*ResourceLink, error) { 31 | if res == nil { 32 | return nil, errors.New("res is nil") 33 | } 34 | 35 | if resType != ResourceTypeLink { 36 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeLink, resType) 37 | } 38 | 39 | link := C.hd_res_get_link(res) 40 | 41 | return &ResourceLink{ 42 | Type: resType, 43 | Connected: bool(C.hd_res_link_get_connected(link)), 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "NixOS Facter"; 3 | 4 | # Add all your dependencies here 5 | inputs = { 6 | blueprint = { 7 | url = "github:numtide/blueprint"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | inputs.systems.follows = "systems"; 10 | }; 11 | systems.url = "github:nix-systems/default"; 12 | nixpkgs.url = "git+https://github.com/NixOS/nixpkgs?shallow=1&ref=nixpkgs-unstable"; 13 | flake-utils.url = "github:numtide/flake-utils"; 14 | flake-utils.inputs.systems.follows = "systems"; 15 | treefmt-nix = { 16 | url = "github:numtide/treefmt-nix"; 17 | inputs.nixpkgs.follows = "nixpkgs"; 18 | }; 19 | disko.url = "github:nix-community/disko"; 20 | disko.inputs.nixpkgs.follows = "nixpkgs"; 21 | 22 | hwinfo.url = "github:numtide/hwinfo"; 23 | hwinfo.inputs.nixpkgs.follows = "nixpkgs"; 24 | hwinfo.inputs.systems.follows = "systems"; 25 | hwinfo.inputs.blueprint.follows = "blueprint"; 26 | }; 27 | 28 | # Keep the magic invocations to minimum. 29 | outputs = 30 | inputs: 31 | inputs.blueprint { 32 | prefix = "nix/"; 33 | inherit inputs; 34 | systems = [ 35 | "aarch64-linux" 36 | "riscv64-linux" 37 | "x86_64-linux" 38 | ]; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_dma.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool hd_res_dma_get_enabled(res_dma_t res) { return res.enabled; } 10 | 11 | // CGO cannot access union type fields, so we do this as a workaround 12 | res_dma_t hd_res_get_dma(hd_res_t *res) { return res->dma; } 13 | 14 | */ 15 | import "C" 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | ) 21 | 22 | type ResourceDma struct { 23 | Type ResourceType `json:"type"` 24 | Base string `json:"base"` 25 | Enabled bool `json:"enabled"` 26 | } 27 | 28 | func (r ResourceDma) ResourceType() ResourceType { 29 | return ResourceTypeDma 30 | } 31 | 32 | func NewResourceDma(res *C.hd_res_t, resType ResourceType) (*ResourceDma, error) { 33 | if res == nil { 34 | return nil, errors.New("res is nil") 35 | } 36 | 37 | if resType != ResourceTypeDma { 38 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeDma, resType) 39 | } 40 | 41 | dma := C.hd_res_get_dma(res) 42 | 43 | return &ResourceDma{ 44 | Type: resType, 45 | Base: fmt.Sprintf("0x%x", uint(dma.base)), 46 | Enabled: bool(C.hd_res_dma_get_enabled(dma)), 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_memory_array_mapped_address.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | import "fmt" 9 | 10 | // SmbiosMemoryArrayMappedAddress captures physical memory array information (consists of several memory devices). 11 | type SmbiosMemoryArrayMappedAddress struct { 12 | Type SmbiosType `json:"-"` 13 | Handle int `json:"handle"` 14 | ArrayHandle int `json:"array_handle"` // memory array this mapping belongs to 15 | StartAddress string `json:"start_address"` // memory range start address 16 | EndAddress string `json:"end_address"` // end address 17 | PartWidth uint16 `json:"part_width"` // number of memory devices 18 | } 19 | 20 | func (s SmbiosMemoryArrayMappedAddress) SmbiosType() SmbiosType { 21 | return s.Type 22 | } 23 | 24 | func NewSmbiosMemArrayMap(info C.smbios_memarraymap_t) (*SmbiosMemoryArrayMappedAddress, error) { 25 | return &SmbiosMemoryArrayMappedAddress{ 26 | Type: SmbiosTypeMemoryArrayMappedAddress, 27 | Handle: int(info.handle), 28 | ArrayHandle: int(info.array_handle), 29 | StartAddress: fmt.Sprintf("0x%x", uint64(info.start_addr)), 30 | EndAddress: fmt.Sprintf("0x%x", uint64(info.end_addr)), 31 | PartWidth: uint16(info.part_width), 32 | }, nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_baud.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_baud_t hd_res_get_baud(hd_res_t *res) { return res->baud; } 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | type ResourceBaud struct { 18 | Type ResourceType `json:"type"` 19 | Speed uint32 `json:"speed"` 20 | Bits uint32 `json:"bits"` 21 | StopBits uint32 `json:"stop_bits"` 22 | Parity byte `json:"parity"` 23 | Handshake byte `json:"handshake"` 24 | } 25 | 26 | func (r ResourceBaud) ResourceType() ResourceType { 27 | return ResourceTypeBaud 28 | } 29 | 30 | func NewResourceBaud(res *C.hd_res_t, resType ResourceType) (*ResourceBaud, error) { 31 | if res == nil { 32 | return nil, errors.New("res is nil") 33 | } 34 | 35 | if resType != ResourceTypeBaud { 36 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeBaud, resType) 37 | } 38 | 39 | baud := C.hd_res_get_baud(res) 40 | 41 | return &ResourceBaud{ 42 | Type: resType, 43 | Speed: uint32(baud.speed), 44 | Bits: uint32(baud.bits), 45 | StopBits: uint32(baud.stopbits), 46 | Parity: byte(baud.parity), 47 | Handshake: byte(baud.handshake), 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_irq.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool hd_res_irq_get_enabled(res_irq_t res) { return res.enabled; } 10 | 11 | // CGO cannot access union type fields, so we do this as a workaround 12 | res_irq_t hd_res_get_irq(hd_res_t *res) { return res->irq; } 13 | */ 14 | import "C" 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | ) 20 | 21 | type ResourceIrq struct { 22 | Type ResourceType `json:"type"` 23 | Base uint16 `json:"base"` 24 | Triggered uint16 `json:"triggered"` 25 | Enabled bool `json:"enabled"` 26 | } 27 | 28 | func (r ResourceIrq) ResourceType() ResourceType { 29 | return ResourceTypeIrq 30 | } 31 | 32 | func NewResourceIrq(res *C.hd_res_t, resType ResourceType) (*ResourceIrq, error) { 33 | if res == nil { 34 | return nil, errors.New("res is nil") 35 | } 36 | 37 | if resType != ResourceTypeIrq { 38 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeIrq, resType) 39 | } 40 | 41 | irq := C.hd_res_get_irq(res) 42 | 43 | return &ResourceIrq{ 44 | Type: resType, 45 | Base: uint16(irq.base), 46 | Triggered: uint16(irq.triggered), 47 | Enabled: bool(C.hd_res_irq_get_enabled(irq)), 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_port_connector.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosPortConnector captures port connector information. 10 | type SmbiosPortConnector struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | PortType *ID `json:"port_type"` 14 | InternalConnectorType *ID `json:"internal_connector_type,omitempty"` 15 | InternalReferenceDesignator string `json:"internal_reference_designator,omitempty"` 16 | ExternalConnectorType *ID `json:"external_connector_type,omitempty"` 17 | ExternalReferenceDesignator string `json:"external_reference_designator,omitempty"` 18 | } 19 | 20 | func (s SmbiosPortConnector) SmbiosType() SmbiosType { 21 | return s.Type 22 | } 23 | 24 | func NewSmbiosConnect(info C.smbios_connect_t) (*SmbiosPortConnector, error) { 25 | return &SmbiosPortConnector{ 26 | Type: SmbiosTypePortConnector, 27 | Handle: int(info.handle), 28 | PortType: NewID(info.port_type), 29 | InternalConnectorType: NewID(info.i_type), 30 | InternalReferenceDesignator: C.GoString(info.i_des), 31 | ExternalConnectorType: NewID(info.x_type), 32 | ExternalReferenceDesignator: C.GoString(info.x_des), 33 | }, nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_memory_array.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | import "fmt" 9 | 10 | // SmbiosMemoryArray captures physical memory array information (consists of several memory devices). 11 | type SmbiosMemoryArray struct { 12 | Type SmbiosType `json:"-"` 13 | Handle int `json:"handle"` 14 | Location *ID `json:"location"` // memory device location 15 | Usage *ID `json:"usage"` // memory usage 16 | ECC *ID `json:"ecc"` // ECC types 17 | MaxSize string `json:"max_size"` // max memory size in KB 18 | ErrorHandle int `json:"error_handle"` // points to error info record; 0xfffe: not supported, 0xffff: no error 19 | Slots uint16 `json:"slots"` // slots or sockets for this device 20 | } 21 | 22 | func (s SmbiosMemoryArray) SmbiosType() SmbiosType { 23 | return s.Type 24 | } 25 | 26 | func NewSmbiosMemArray(info C.smbios_memarray_t) (*SmbiosMemoryArray, error) { 27 | return &SmbiosMemoryArray{ 28 | Type: SmbiosTypeMemoryArray, 29 | Handle: int(info.handle), 30 | Location: NewID(info.location), 31 | Usage: NewID(info.use), 32 | ECC: NewID(info.ecc), 33 | MaxSize: fmt.Sprintf("0x%x", uint(info.max_size)), 34 | ErrorHandle: int(info.error_handle), 35 | Slots: uint16(info.slots), 36 | }, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_frame_buffer.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_framebuffer_t hd_res_get_framebuffer(hd_res_t *res) { return res->framebuffer; } 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | type ResourceFrameBuffer struct { 18 | Type ResourceType `json:"type"` 19 | Width uint32 `json:"width"` 20 | Height uint32 `json:"height"` 21 | BytesPerLine uint16 `json:"bytes_per_line"` 22 | ColorBits uint16 `json:"color_bits"` 23 | Mode uint16 `json:"mode"` 24 | } 25 | 26 | func (r ResourceFrameBuffer) ResourceType() ResourceType { 27 | return ResourceTypeFramebuffer 28 | } 29 | 30 | func NewResourceFrameBuffer(res *C.hd_res_t, resType ResourceType) (*ResourceFrameBuffer, error) { 31 | if res == nil { 32 | return nil, errors.New("res is nil") 33 | } 34 | 35 | if resType != ResourceTypeFramebuffer { 36 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeFramebuffer, resType) 37 | } 38 | 39 | fb := C.hd_res_get_framebuffer(res) 40 | 41 | return &ResourceFrameBuffer{ 42 | Type: resType, 43 | Width: uint32(fb.width), 44 | Height: uint32(fb.height), 45 | BytesPerLine: uint16(fb.bytes_p_line), 46 | ColorBits: uint16(fb.colorbits), 47 | Mode: uint16(fb.mode), 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_wlan.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_wlan_t hd_res_get_wlan(hd_res_t *res) { return res->wlan; } 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | type ResourceWlan struct { 18 | Type ResourceType `json:"type"` 19 | Channels []string `json:"channels,omitempty"` 20 | Frequencies []string `json:"frequencies,omitempty"` 21 | BitRates []string `json:"bit_rates,omitempty"` 22 | AuthModes []string `json:"auth_modes,omitempty"` 23 | EncModes []string `json:"enc_modes,omitempty"` 24 | } 25 | 26 | func (r ResourceWlan) ResourceType() ResourceType { 27 | return ResourceTypeWlan 28 | } 29 | 30 | func NewResourceWlan(res *C.hd_res_t, resType ResourceType) (*ResourceWlan, error) { 31 | if res == nil { 32 | return nil, errors.New("res is nil") 33 | } 34 | 35 | if resType != ResourceTypeWlan { 36 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeWlan, resType) 37 | } 38 | 39 | wlan := C.hd_res_get_wlan(res) 40 | 41 | return &ResourceWlan{ 42 | Type: resType, 43 | Channels: ReadStringList(wlan.channels), 44 | Frequencies: ReadStringList(wlan.frequencies), 45 | BitRates: ReadStringList(wlan.bitrates), 46 | AuthModes: ReadStringList(wlan.auth_modes), 47 | EncModes: ReadStringList(wlan.enc_modes), 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_onboard.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | char* smbios_onboard_get_name(smbios_onboard_t sm, int i) { return sm.dev[i].name; } 8 | hd_id_t smbios_onboard_get_type(smbios_onboard_t sm, int i) { return sm.dev[i].type; } 9 | unsigned smbios_onboard_get_status(smbios_onboard_t sm, int i) { return sm.dev[i].status; } 10 | */ 11 | import "C" 12 | 13 | type OnboardDevice struct { 14 | Name string `json:"name"` 15 | Type *ID `json:"type"` 16 | Enabled bool `json:"enabled"` 17 | } 18 | 19 | // SmbiosOnboard captures overall system related information. 20 | type SmbiosOnboard struct { 21 | Type SmbiosType `json:"-"` 22 | Handle int `json:"handle"` 23 | Devices []OnboardDevice `json:"devices,omitempty"` 24 | } 25 | 26 | func (s SmbiosOnboard) SmbiosType() SmbiosType { 27 | return s.Type 28 | } 29 | 30 | func NewSmbiosOnboard(info C.smbios_onboard_t) (*SmbiosOnboard, error) { 31 | count := int(info.dev_len) 32 | devices := make([]OnboardDevice, 0, count) 33 | 34 | for i := range count { 35 | devices = append(devices, OnboardDevice{ 36 | Name: C.GoString(C.smbios_onboard_get_name(info, C.int(i))), 37 | Type: NewID(C.smbios_onboard_get_type(info, C.int(i))), 38 | Enabled: uint(C.smbios_onboard_get_status(info, C.int(i))) == 1, 39 | }) 40 | } 41 | 42 | return &SmbiosOnboard{ 43 | Type: SmbiosTypeOnboard, 44 | Handle: int(info.handle), 45 | Devices: devices, 46 | }, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_io.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool hd_res_io_get_enabled(res_io_t res) { return res.enabled; } 10 | unsigned hd_res_io_get_access(res_io_t res) { return res.access; } 11 | 12 | // CGO cannot access union type fields, so we do this as a workaround 13 | res_io_t hd_res_get_io(hd_res_t *res) { return res->io; } 14 | */ 15 | import "C" 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | ) 21 | 22 | type ResourceIO struct { 23 | Type ResourceType `json:"type"` 24 | Base uint64 `json:"base"` 25 | Range uint64 `json:"range"` 26 | Enabled bool `json:"enabled"` 27 | Access AccessFlags `json:"access"` 28 | } 29 | 30 | func (r ResourceIO) ResourceType() ResourceType { 31 | return ResourceTypeIo 32 | } 33 | 34 | func NewResourceIO(res *C.hd_res_t, resType ResourceType) (*ResourceIO, error) { 35 | if res == nil { 36 | return nil, errors.New("res is nil") 37 | } 38 | 39 | if resType != ResourceTypeIo { 40 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeIo, resType) 41 | } 42 | 43 | io := C.hd_res_get_io(res) 44 | 45 | return &ResourceIO{ 46 | Type: resType, 47 | Base: uint64(io.base), 48 | Range: uint64(io._range), 49 | Enabled: bool(C.hd_res_io_get_enabled(io)), 50 | Access: AccessFlags(C.hd_res_io_get_access(io)), 51 | }, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_size.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_size_t hd_res_get_size(hd_res_t *res) { return res->size; } 9 | 10 | */ 11 | import "C" 12 | 13 | import ( 14 | "errors" 15 | "fmt" 16 | ) 17 | 18 | //go:generate enumer -type=SizeUnit -json --transform=snake -trimprefix SizeUnit --output=./resource_enum_size_unit.go 19 | type SizeUnit uint //nolint:recvcheck 20 | 21 | const ( 22 | SizeUnitCm SizeUnit = iota 23 | SizeUnitCinch 24 | SizeUnitByte 25 | SizeUnitSectors 26 | 27 | SizeUnitKbyte 28 | SizeUnitMbyte 29 | SizeUnitGbyte 30 | SizeUnitMm 31 | ) 32 | 33 | type ResourceSize struct { 34 | Type ResourceType `json:"type"` 35 | Unit SizeUnit `json:"unit"` 36 | Value1 uint64 `json:"value_1"` 37 | Value2 uint64 `json:"value_2,omitempty"` 38 | } 39 | 40 | func (r ResourceSize) ResourceType() ResourceType { 41 | return ResourceTypeSize 42 | } 43 | 44 | func NewResourceSize(res *C.hd_res_t, resType ResourceType) (*ResourceSize, error) { 45 | if res == nil { 46 | return nil, errors.New("res is nil") 47 | } 48 | 49 | if resType != ResourceTypeSize { 50 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeSize, resType) 51 | } 52 | 53 | size := C.hd_res_get_size(res) 54 | 55 | return &ResourceSize{ 56 | Type: resType, 57 | Unit: SizeUnit(size.unit), 58 | Value1: uint64(size.val1), 59 | Value2: uint64(size.val2), 60 | }, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/hwinfo/iommu.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | ) 9 | 10 | const ( 11 | iommuSysfsPath = "/sys/kernel/iommu_groups" 12 | ) 13 | 14 | type IOMMUGroup int 15 | 16 | func IOMMUGroups() (map[string]IOMMUGroup, error) { 17 | groups, err := os.ReadDir(iommuSysfsPath) 18 | if err != nil { 19 | return nil, fmt.Errorf("failed to read iommu groups from sysfs: %w", err) 20 | } 21 | 22 | result := make(map[string]IOMMUGroup) 23 | 24 | for _, group := range groups { 25 | if !group.IsDir() { 26 | return nil, fmt.Errorf("non-directory entry found in %s: %s", iommuSysfsPath, group.Name()) 27 | } 28 | 29 | groupID, err := strconv.Atoi(group.Name()) 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to parse iommu group id '%s': %w", group.Name(), err) 32 | } 33 | 34 | devicesPath := filepath.Join(iommuSysfsPath, group.Name(), "devices") 35 | 36 | devices, err := os.ReadDir(devicesPath) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to read devices from iommu group %s: %w", devicesPath, err) 39 | } 40 | 41 | for _, device := range devices { 42 | devicePath := filepath.Join(devicesPath, device.Name()) 43 | 44 | resolvedPath, err := filepath.EvalSymlinks(devicePath) 45 | if err != nil { 46 | return nil, fmt.Errorf("failed to resolve device symlink '%s': %w", devicePath, err) 47 | } 48 | 49 | // sysfs id -> iommu group id 50 | result[resolvedPath[4:]] = IOMMUGroup(groupID) 51 | } 52 | } 53 | 54 | return result, nil 55 | } 56 | -------------------------------------------------------------------------------- /nix/packages/nixos-facter/tests/disko.nix: -------------------------------------------------------------------------------- 1 | { 2 | disko = { 3 | devices = { 4 | disk = { 5 | main = { 6 | device = "/dev/vdb"; 7 | type = "disk"; 8 | content = { 9 | type = "gpt"; 10 | partitions = { 11 | ESP = { 12 | size = "500M"; 13 | type = "EF00"; 14 | content = { 15 | type = "filesystem"; 16 | format = "vfat"; 17 | mountpoint = "/boot"; 18 | }; 19 | }; 20 | root = { 21 | end = "-1G"; 22 | content = { 23 | type = "filesystem"; 24 | format = "ext4"; 25 | mountpoint = "/"; 26 | }; 27 | }; 28 | encryptedSwap = { 29 | size = "10M"; 30 | content = { 31 | type = "swap"; 32 | randomEncryption = true; 33 | priority = 100; # prefer to encrypt as long as we have space for it 34 | }; 35 | }; 36 | plainSwap = { 37 | size = "100%"; 38 | content = { 39 | type = "swap"; 40 | discardPolicy = "both"; 41 | resumeDevice = true; # resume from hiberation from this device 42 | }; 43 | }; 44 | }; 45 | }; 46 | }; 47 | }; 48 | }; 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /pkg/boot/boot.go: -------------------------------------------------------------------------------- 1 | // Package boot contains utilities for detecting boot-related information. 2 | package boot 3 | 4 | import ( 5 | "log/slog" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | efiFirmwarePath = "/sys/firmware/efi" 13 | efiPlatformSizePath = "/sys/firmware/efi/fw_platform_size" 14 | ) 15 | 16 | // UEFIInfo contains information about UEFI firmware support. 17 | type UEFIInfo struct { 18 | // Supported indicates whether the system booted with UEFI. 19 | Supported bool `json:"supported"` 20 | 21 | // PlatformSize indicates the firmware platform size in bits (32 or 64). 22 | // Only populated when Supported is true. 23 | PlatformSize *uint8 `json:"platform_size,omitempty"` 24 | } 25 | 26 | // DetectUEFI detects UEFI boot information. 27 | func DetectUEFI() (*UEFIInfo, error) { 28 | info := &UEFIInfo{} 29 | 30 | // Check if the EFI firmware directory exists 31 | if stat, err := os.Stat(efiFirmwarePath); err != nil || !stat.IsDir() { 32 | slog.Debug("UEFI boot not detected", "path", efiFirmwarePath) 33 | return info, nil 34 | } 35 | 36 | info.Supported = true 37 | slog.Debug("UEFI boot detected", "path", efiFirmwarePath) 38 | 39 | // Detect platform size (32 or 64 bit) 40 | if data, err := os.ReadFile(efiPlatformSizePath); err == nil { 41 | sizeStr := strings.TrimSpace(string(data)) 42 | if size, err := strconv.ParseUint(sizeStr, 10, 8); err == nil { 43 | size8 := uint8(size) 44 | info.PlatformSize = &size8 45 | slog.Debug("UEFI platform size detected", "bits", size) 46 | } 47 | } 48 | 49 | return info, nil 50 | } 51 | -------------------------------------------------------------------------------- /nix/formatter.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | flake, 4 | inputs, 5 | ... 6 | }: 7 | let 8 | mod = inputs.treefmt-nix.lib.evalModule pkgs { 9 | projectRootFile = ".git/config"; 10 | 11 | programs = { 12 | nixfmt.enable = true; 13 | deadnix.enable = true; 14 | gofumpt.enable = true; 15 | prettier.enable = true; 16 | statix.enable = true; 17 | shellcheck.enable = true; 18 | }; 19 | 20 | settings = { 21 | global.excludes = [ 22 | "LICENSE" 23 | # unsupported extensions 24 | "*.{gif,png,svg,tape,mts,lock,mod,sum,toml,env,envrc,gitignore}" 25 | ]; 26 | 27 | formatter = { 28 | deadnix = { 29 | priority = 1; 30 | }; 31 | 32 | statix = { 33 | priority = 2; 34 | }; 35 | 36 | nixfmt = { 37 | priority = 3; 38 | }; 39 | 40 | prettier = { 41 | options = [ 42 | "--tab-width" 43 | "4" 44 | ]; 45 | includes = [ "*.{css,html,js,json,jsx,md,mdx,scss,ts,yaml}" ]; 46 | }; 47 | }; 48 | }; 49 | }; 50 | 51 | wrapper = mod.config.build.wrapper // { 52 | passthru.tests.check = mod.config.build.check flake; 53 | }; 54 | 55 | unsupported = pkgs.writeShellApplication { 56 | name = "unsupported-platform"; 57 | text = '' 58 | echo "nix fmt is not supported on ${pkgs.stdenv.hostPlatform.system}"; 59 | ''; 60 | }; 61 | in 62 | # nixfmt-rfc-style is based on Haskell, which is broke on RiscV currently 63 | if pkgs.stdenv.hostPlatform.isRiscV then unsupported else wrapper 64 | -------------------------------------------------------------------------------- /.github/workflows/update-vendor-hash.yml: -------------------------------------------------------------------------------- 1 | name: Update vendorHash 2 | on: pull_request 3 | jobs: 4 | dependabot: 5 | runs-on: ubuntu-latest 6 | if: ${{ github.actor == 'dependabot[bot]' }} 7 | steps: 8 | - uses: actions/create-github-app-token@v2 9 | id: app-token 10 | with: 11 | app-id: ${{ secrets.APP_ID }} 12 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 13 | - uses: actions/checkout@v6 14 | with: 15 | ref: ${{ github.event.pull_request.head.sha }} 16 | fetch-depth: 0 17 | token: ${{ steps.app-token.outputs.token }} 18 | - name: Install Nix 19 | uses: cachix/install-nix-action@v31 20 | with: 21 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 22 | nix_path: nixpkgs=channel:nixos-unstable 23 | - name: Update checksum 24 | env: 25 | HEAD_REF: ${{ github.head_ref }} 26 | run: | 27 | ./.github/workflows/update-vendor-hash.sh 28 | # git push if we have a diff 29 | if [[ -n $(git diff) ]]; then 30 | git add nix/packages/nixos-facter/goVendorHash.txt 31 | git config --global user.email "<49699333+dependabot[bot]@users.noreply.github.com>" 32 | git config --global user.name "dependabot[bot]" 33 | git commit -m "update vendorHash" 34 | git push origin HEAD:"${HEAD_REF}" 35 | fi 36 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_monitor.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool hd_res_monitor_get_interlaced(res_monitor_t res) { return res.interlaced; } 10 | 11 | // CGO cannot access union type fields, so we do this as a workaround 12 | res_monitor_t hd_res_get_monitor(hd_res_t *res) { return res->monitor; } 13 | */ 14 | import "C" 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | ) 20 | 21 | type ResourceMonitor struct { 22 | Type ResourceType `json:"type"` 23 | Width uint32 `json:"width"` 24 | Height uint32 `json:"height"` 25 | VerticalFrequency uint16 `json:"vertical_frequency"` 26 | Interlaced bool `json:"interlaced"` 27 | } 28 | 29 | func (r ResourceMonitor) ResourceType() ResourceType { 30 | return ResourceTypeMonitor 31 | } 32 | 33 | func NewResourceMonitor(res *C.hd_res_t, resType ResourceType) (*ResourceMonitor, error) { 34 | if res == nil { 35 | return nil, errors.New("res is nil") 36 | } 37 | 38 | if resType != ResourceTypeMonitor { 39 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeMonitor, resType) 40 | } 41 | 42 | monitor := C.hd_res_get_monitor(res) 43 | 44 | return &ResourceMonitor{ 45 | Type: resType, 46 | Width: uint32(monitor.width), 47 | Height: uint32(monitor.height), 48 | VerticalFrequency: uint16(monitor.vfreq), 49 | Interlaced: bool(C.hd_res_monitor_get_interlaced(monitor)), 50 | }, nil 51 | } 52 | -------------------------------------------------------------------------------- /nix/packages/nixos-facter/package.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | systemdMinimal, 4 | hwinfo, 5 | gcc, 6 | makeWrapper, 7 | pkg-config, 8 | stdenv, 9 | buildGo124Module, 10 | versionCheckHook, 11 | }: 12 | let 13 | fs = lib.fileset; 14 | in 15 | buildGo124Module (final: { 16 | pname = "nixos-facter"; 17 | version = "0.4.2"; 18 | 19 | src = fs.toSource { 20 | root = ../../..; 21 | fileset = fs.unions [ 22 | ../../../cmd 23 | ../../../go.mod 24 | ../../../go.sum 25 | ../../../main.go 26 | ../../../pkg 27 | ]; 28 | }; 29 | 30 | vendorHash = lib.fileContents ./goVendorHash.txt; 31 | 32 | buildInputs = [ 33 | systemdMinimal 34 | hwinfo 35 | ]; 36 | 37 | nativeBuildInputs = [ 38 | gcc 39 | makeWrapper 40 | pkg-config 41 | versionCheckHook 42 | ]; 43 | 44 | ldflags = [ 45 | "-s" 46 | "-w" 47 | "-X github.com/numtide/nixos-facter/pkg/build.Name=${final.pname}" 48 | "-X github.com/numtide/nixos-facter/pkg/build.Version=v${final.version}" 49 | "-X github.com/numtide/nixos-facter/pkg/build.System=${stdenv.hostPlatform.system}" 50 | ]; 51 | 52 | doInstallCheck = true; 53 | postInstall = 54 | let 55 | binPath = lib.makeBinPath [ 56 | systemdMinimal 57 | ]; 58 | in 59 | '' 60 | wrapProgram "$out/bin/nixos-facter" \ 61 | --prefix PATH : "${binPath}" 62 | ''; 63 | 64 | meta = with lib; { 65 | description = "nixos-facter: declarative nixos-generate-config"; 66 | homepage = "https://github.com/nix-community/nixos-facter"; 67 | license = licenses.mit; 68 | mainProgram = "nixos-facter"; 69 | }; 70 | }) 71 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_board.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | import "unsafe" 9 | 10 | // SmbiosBoard captures motherboard related information. 11 | type SmbiosBoard struct { 12 | Type SmbiosType `json:"-"` 13 | Handle int `json:"handle"` 14 | Manufacturer string `json:"manufacturer"` 15 | Product string `json:"product"` 16 | Version string `json:"version"` 17 | Serial string `json:"-"` // omit from json output 18 | AssetTag string `json:"-"` 19 | BoardType *ID `json:"board_type"` 20 | Features []string `json:"features"` 21 | Location string `json:"location"` // location in chassis 22 | Chassis int `json:"chassis"` // handle of chassis 23 | Objects []int `json:"objects,omitempty"` // array of object handles 24 | } 25 | 26 | func (s SmbiosBoard) SmbiosType() SmbiosType { 27 | return s.Type 28 | } 29 | 30 | func NewSmbiosBoardInfo(info C.smbios_boardinfo_t) (*SmbiosBoard, error) { 31 | return &SmbiosBoard{ 32 | Type: SmbiosTypeBoard, 33 | Handle: int(info.handle), 34 | Manufacturer: C.GoString(info.manuf), 35 | Product: C.GoString(info.product), 36 | Version: C.GoString(info.version), 37 | Serial: C.GoString(info.serial), 38 | AssetTag: C.GoString(info.asset), 39 | BoardType: NewID(info.board_type), 40 | Features: ReadStringList(info.feature.str), 41 | Location: C.GoString(info.location), 42 | Chassis: int(info.chassis), 43 | Objects: ReadIntArray(unsafe.Pointer(info.objects), int(info.objects_len)), 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_disk_geo.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | res_disk_geo_t hd_res_get_disk_geo(hd_res_t *res) { return res->disk_geo; } 9 | */ 10 | import "C" 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | //go:generate enumer -type=GeoType -json -transform=snake -trimprefix GeoType -output=./resource_enum_geo_type.go 18 | type GeoType uint //nolint:recvcheck 19 | 20 | const ( 21 | GeoTypePhysical GeoType = iota 22 | GeoTypeLogical 23 | GeoTypeBiosEdd 24 | GeoTypeBiosLegacy 25 | ) 26 | 27 | type ResourceDiskGeo struct { 28 | Type ResourceType `json:"type"` 29 | Cylinders uint32 `json:"cylinders"` 30 | Heads uint8 `json:"heads"` 31 | Sectors uint32 `json:"sectors"` 32 | Size string `json:"size"` 33 | GeoType GeoType `json:"geo_type"` 34 | } 35 | 36 | func (r ResourceDiskGeo) ResourceType() ResourceType { 37 | return ResourceTypeDiskGeo 38 | } 39 | 40 | func NewResourceDiskGeo(res *C.hd_res_t, resType ResourceType) (*ResourceDiskGeo, error) { 41 | if res == nil { 42 | return nil, errors.New("res is nil") 43 | } 44 | 45 | if resType != ResourceTypeDiskGeo { 46 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeDiskGeo, resType) 47 | } 48 | 49 | disk := C.hd_res_get_disk_geo(res) 50 | 51 | return &ResourceDiskGeo{ 52 | Type: resType, 53 | Cylinders: uint32(disk.cyls), 54 | Heads: uint8(disk.heads), 55 | Sectors: uint32(disk.sectors), 56 | Size: fmt.Sprintf("0x%x", uint64(disk.size)), 57 | GeoType: GeoType(disk.geotype), 58 | }, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_memory_error.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | import "fmt" 9 | 10 | // SmbiosMemoryError captures 32-bit memory error information. 11 | type SmbiosMemoryError struct { 12 | Type SmbiosType `json:"-"` 13 | Handle int `json:"handle"` 14 | ErrorType *ID `json:"error_type"` // error type memory 15 | Granularity *ID `json:"granularity"` // memory array or memory partition 16 | Operation *ID `json:"operation"` // mem operation causing the rror 17 | Syndrome uint32 `json:"syndrome"` // vendor-specific ECC syndrome; 0: unknown 18 | ArrayAddress string `json:"array_address"` // fault address relative to mem array; 0x80000000: unknown 19 | DeviceAddress string `json:"device_address"` // fault address relative to mem array; 0x80000000: unknown 20 | Range uint32 `json:"range"` // range within which the error can be determined; 0x80000000: unknown 21 | } 22 | 23 | func (s SmbiosMemoryError) SmbiosType() SmbiosType { 24 | return s.Type 25 | } 26 | 27 | func NewSmbiosMemError(info C.smbios_memerror_t) (*SmbiosMemoryError, error) { 28 | return &SmbiosMemoryError{ 29 | Type: SmbiosTypeMemoryError, 30 | Handle: int(info.handle), 31 | ErrorType: NewID(info.err_type), 32 | Granularity: NewID(info.granularity), 33 | Operation: NewID(info.operation), 34 | Syndrome: uint32(info.syndrome), 35 | ArrayAddress: fmt.Sprintf("0x%x", uint(info.array_addr)), 36 | DeviceAddress: fmt.Sprintf("0x%x", uint(info.device_addr)), 37 | Range: uint32(info._range), 38 | }, nil 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | deploy: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | concurrency: 19 | group: ${{ github.workflow }} 20 | steps: 21 | - uses: actions/checkout@v6 22 | with: 23 | fetch-depth: 0 24 | - uses: cachix/install-nix-action@v31 25 | with: 26 | extra_nix_config: | 27 | accept-flake-config = true 28 | experimental-features = nix-command flakes 29 | - name: Configure Git user 30 | run: | 31 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 32 | git config --local user.name "github-actions[bot]" 33 | - name: Deploy main 34 | if: ${{ github.ref_name == 'main' }} 35 | run: | 36 | nix develop .#docs --command bash -c "mike deploy -p -u main" 37 | - name: Deploy version 38 | if: startsWith(github.ref, 'refs/tags/v') 39 | run: | 40 | REF_NAME=${{ github.ref_name }} 41 | MAJOR_MINOR=${REF_NAME%.*} 42 | # strip the leading v from the ref_name 43 | # update the latest alias 44 | nix develop .#docs --command bash -c "mike deploy -p -u ${MAJOR_MINOR} latest" 45 | -------------------------------------------------------------------------------- /docs/content/assets/images/hero.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_memory64_error.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | import "fmt" 9 | 10 | // SmbiosMemory64Error captures 32-bit memory error information. 11 | type SmbiosMemory64Error struct { 12 | Type SmbiosType `json:"-"` 13 | Handle int `json:"handle"` 14 | ErrorType *ID `json:"error_type"` // error type memory 15 | Granularity *ID `json:"granularity"` // memory array or memory partition 16 | Operation *ID `json:"operation"` // mem operation causing the rror 17 | Syndrome uint32 `json:"syndrome"` // vendor-specific ECC syndrome; 0: unknown 18 | ArrayAddress string `json:"array_address"` // fault address relative to mem array; 0x80000000: unknown 19 | DeviceAddress string `json:"device_address"` // fault address relative to mem array; 0x80000000: unknown 20 | Range uint32 `json:"range"` // range within which the error can be determined; 0x80000000: unknown 21 | } 22 | 23 | func (s SmbiosMemory64Error) SmbiosType() SmbiosType { 24 | return s.Type 25 | } 26 | 27 | func NewSmbiosMem64Error(info C.smbios_mem64error_t) (*SmbiosMemory64Error, error) { 28 | return &SmbiosMemory64Error{ 29 | Type: SmbiosTypeMemory64Error, 30 | Handle: int(info.handle), 31 | ErrorType: NewID(info.err_type), 32 | Granularity: NewID(info.granularity), 33 | Operation: NewID(info.operation), 34 | Syndrome: uint32(info.syndrome), 35 | ArrayAddress: fmt.Sprintf("0x%x", uint(info.array_addr)), 36 | DeviceAddress: fmt.Sprintf("0x%x", uint(info.device_addr)), 37 | Range: uint32(info._range), 38 | }, nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/hwinfo/utils.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | import "unsafe" 9 | 10 | func ReadStringList(list *C.str_list_t) []string { 11 | var result []string 12 | for entry := list; entry != nil; entry = entry.next { 13 | result = append(result, C.GoString(entry.str)) 14 | } 15 | 16 | return result 17 | } 18 | 19 | func ReadUint64Array(arr unsafe.Pointer, length int) []uint64 { 20 | start := uintptr(arr) 21 | 22 | result := make([]uint64, length) 23 | for i := range result { 24 | next := start + uintptr(i*C.sizeof_uint64_t) 25 | result[i] = *((*uint64)(unsafe.Pointer(next))) //nolint:govet 26 | } 27 | 28 | return result 29 | } 30 | 31 | func ReadUintArray(arr unsafe.Pointer, length int) []uint { 32 | start := uintptr(arr) 33 | 34 | result := make([]uint, length) 35 | for i := range result { 36 | next := start + uintptr(i*C.sizeof_uint) 37 | result[i] = *((*uint)(unsafe.Pointer(next))) //nolint:govet 38 | } 39 | 40 | return result 41 | } 42 | 43 | func ReadIntArray(arr unsafe.Pointer, length int) []int { 44 | // TODO see if we can use generics to combine some of these methods 45 | start := uintptr(arr) 46 | 47 | result := make([]int, length) 48 | for i := range result { 49 | next := start + uintptr(i*C.sizeof_uint) 50 | result[i] = *((*int)(unsafe.Pointer(next))) //nolint:govet 51 | } 52 | 53 | return result 54 | } 55 | 56 | func ReadByteArray(arr unsafe.Pointer, length int) []byte { 57 | start := uintptr(arr) 58 | 59 | result := make([]byte, length) 60 | for i := range result { 61 | next := start + uintptr(i*C.sizeof_uint) 62 | result[i] = *((*byte)(unsafe.Pointer(next))) //nolint:govet 63 | } 64 | 65 | return result 66 | } 67 | -------------------------------------------------------------------------------- /pkg/hwinfo/driver_info_module.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool driver_info_module_is_active(driver_info_module_t info) { return info.active; } 10 | bool driver_info_module_is_modprobe(driver_info_module_t info) { return info.modprobe; } 11 | */ 12 | import "C" 13 | 14 | type DriverInfoModule struct { 15 | Type DriverInfoType `json:"type,omitempty"` 16 | // actual driver database entries 17 | DBEntry0 []string `json:"db_entry_0,omitempty"` 18 | DBEntry1 []string `json:"db_entry_1,omitempty"` 19 | 20 | Active bool `json:"active"` // if the module is currently active 21 | Modprobe bool `json:"modprobe"` // modprobe or insmod 22 | Names []string `json:"names"` // (ordered) list of module names 23 | ModuleArgs []string `json:"module_args"` // list of module args (corresponds to the module name list) 24 | Conf string `json:"conf"` // conf.modules entry, if any (e.g., for sb.o) 25 | } 26 | 27 | func (d DriverInfoModule) DriverInfoType() DriverInfoType { 28 | return DriverInfoTypeModule 29 | } 30 | 31 | func NewDriverInfoModule(info C.driver_info_module_t) DriverInfoModule { 32 | return DriverInfoModule{ 33 | Type: DriverInfoTypeModule, 34 | DBEntry0: ReadStringList(info.hddb0), 35 | DBEntry1: ReadStringList(info.hddb1), 36 | Active: bool(C.driver_info_module_is_active(info)), 37 | Modprobe: bool(C.driver_info_module_is_modprobe(info)), 38 | Names: ReadStringList(info.names), 39 | ModuleArgs: ReadStringList(info.mod_args), 40 | Conf: C.GoString(info.conf), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_memory_device_mapped_address.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | import "fmt" 9 | 10 | // SmbiosMemoryDeviceMappedAddress captures physical memory array information (consists of several memory devices). 11 | // 12 | //nolint:lll 13 | type SmbiosMemoryDeviceMappedAddress struct { 14 | Type SmbiosType `json:"-"` 15 | Handle int `json:"handle"` 16 | MemoryDeviceHandle int `json:"memory_device_handle"` 17 | ArrayMapHandle int `json:"array_map_handle"` 18 | StartAddress string `json:"start_address"` 19 | EndAddress string `json:"end_address"` 20 | RowPosition uint16 `json:"row_position"` // position of the referenced memory device in a row of the address partition 21 | InterleavePosition uint16 `json:"interleave_position"` // dto, in an interleave 22 | InterleaveDepth uint16 `json:"interleave_depth"` // number of consecutive rows 23 | } 24 | 25 | func (s SmbiosMemoryDeviceMappedAddress) SmbiosType() SmbiosType { 26 | return s.Type 27 | } 28 | 29 | func NewSmbiosMemDeviceMap(info C.smbios_memdevicemap_t) (*SmbiosMemoryDeviceMappedAddress, error) { 30 | return &SmbiosMemoryDeviceMappedAddress{ 31 | Type: SmbiosTypeMemoryDeviceMappedAddress, 32 | Handle: int(info.handle), 33 | MemoryDeviceHandle: int(info.memdevice_handle), 34 | ArrayMapHandle: int(info.arraymap_handle), 35 | StartAddress: fmt.Sprintf("0x%x", uint64(info.start_addr)), 36 | EndAddress: fmt.Sprintf("0x%x", uint64(info.end_addr)), 37 | RowPosition: uint16(info.row_pos), 38 | InterleavePosition: uint16(info.interleave_pos), 39 | InterleaveDepth: uint16(info.interleave_depth), 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 3 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 5 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 6 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 7 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 8 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 9 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 10 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 12 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 13 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 14 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 17 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /pkg/virt/virt.go: -------------------------------------------------------------------------------- 1 | // Package virt contains utilities for detecting virtualized environments. 2 | // It has been adapted from [systemd]. 3 | // 4 | // [systemd]: https://github.com/systemd/systemd/blob/main/src/basic/virt.c 5 | package virt 6 | 7 | import ( 8 | "fmt" 9 | "log/slog" 10 | "os/exec" 11 | "strings" 12 | ) 13 | 14 | // Type represents various virtualisation and container types. 15 | // 16 | //go:generate enumer -type=Type -json -text -transform=snake -trimprefix Type -output=./virt_enum_type.go 17 | type Type int //nolint:recvcheck 18 | 19 | const ( 20 | TypeNone Type = iota 21 | TypeKvm 22 | TypeAmazon 23 | TypeQemu 24 | TypeBochs 25 | TypeXen 26 | TypeUml 27 | TypeVmware 28 | TypeOracle 29 | TypeMicrosoft 30 | TypeZvm 31 | TypeParallels 32 | TypeBhyve 33 | TypeQnx 34 | TypeAcrn 35 | TypePowervm 36 | TypeApple 37 | TypeSre 38 | TypeGoogle 39 | TypeVmOther 40 | TypeSystemdNspawn 41 | TypeLxcLibvirt 42 | TypeLxc 43 | TypeOpenvz 44 | TypeDocker 45 | TypePodman 46 | TypeRkt 47 | TypeWsl 48 | TypeProot 49 | TypePouch 50 | TypeContainerOther 51 | ) 52 | 53 | // Detect identifies the virtualisation type of the current system. 54 | // Returns the detected Type and an error if detection fails. 55 | func Detect() (Type, error) { 56 | out, err := exec.Command("systemd-detect-virt").CombinedOutput() 57 | outStr := strings.Trim(string(out), "\n") 58 | 59 | // note: systemd-detect-virt exits with status 1 when "none" is detected 60 | //nolint:staticcheck 61 | if !(outStr == "none" || err == nil) { 62 | slog.Error("failed to detect virtualisation type", "output", out) 63 | 64 | return TypeNone, fmt.Errorf("failed to detect virtualisation type: %w", err) 65 | } 66 | 67 | // we use snake case, but systemd-detect-virt uses hyphen case 68 | virtType := strings.ReplaceAll(outStr, "-", "_") 69 | 70 | return TypeString(virtType) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_chassis.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | import "fmt" 9 | 10 | // SmbiosChassis captures motherboard related information. 11 | type SmbiosChassis struct { 12 | Type SmbiosType `json:"-"` 13 | Handle int `json:"handle"` 14 | Manufacturer string `json:"manufacturer"` 15 | Version string `json:"version"` 16 | Serial string `json:"-"` // omit from json output 17 | AssetTag string `json:"-"` // asset tag 18 | ChassisType *ID `json:"chassis_type"` 19 | LockPresent bool `json:"lock_present"` // true: lock present, false: not present or unknown 20 | BootupState *ID `json:"bootup_state"` 21 | PowerState *ID `json:"power_state"` // power supply state (at last boot) 22 | ThermalState *ID `json:"thermal_state"` // thermal state (at last boot) 23 | SecurityState *ID `json:"security_state"` // security state (at last boot) 24 | OEM string `json:"oem"` // oem-specific information" 25 | } 26 | 27 | func (s SmbiosChassis) SmbiosType() SmbiosType { 28 | return s.Type 29 | } 30 | 31 | func NewSmbiosChassis(info C.smbios_chassis_t) (*SmbiosChassis, error) { 32 | return &SmbiosChassis{ 33 | Type: SmbiosTypeChassis, 34 | Handle: int(info.handle), 35 | Manufacturer: C.GoString(info.manuf), 36 | Version: C.GoString(info.version), 37 | Serial: C.GoString(info.serial), 38 | AssetTag: C.GoString(info.asset), 39 | ChassisType: NewID(info.ch_type), 40 | LockPresent: uint(info.lock) == 1, 41 | BootupState: NewID(info.bootup), 42 | PowerState: NewID(info.power), 43 | ThermalState: NewID(info.thermal), 44 | SecurityState: NewID(info.security), 45 | OEM: fmt.Sprintf("0x%x", uint(info.oem)), 46 | }, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_fc.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool hd_res_fc_get_wwpn_ok(res_fc_t res) { return res.wwpn_ok; } 10 | bool hd_res_fc_get_fcp_lun_ok(res_fc_t res) { return res.fcp_lun_ok; } 11 | bool hd_res_fc_get_port_id_ok(res_fc_t res) { return res.port_id_ok; } 12 | 13 | // CGO cannot access union type fields, so we do this as a workaround 14 | res_fc_t hd_res_get_fc(hd_res_t *res) { return res->fc; } 15 | */ 16 | import "C" 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | ) 22 | 23 | // todo what is FC? 24 | type ResourceFc struct { 25 | Type ResourceType `json:"type"` 26 | WwpnOk bool `json:"wwpn_ok"` 27 | FcpLunOk bool `json:"fcp_lun_ok"` 28 | PortIDOk bool `json:"port_id_ok"` 29 | Wwpn uint64 `json:"wwpn"` 30 | FcpLun uint64 `json:"fcp_lun"` 31 | PortID uint32 `json:"port_id"` 32 | ControllerID byte `json:"controller_id"` 33 | } 34 | 35 | func (r ResourceFc) ResourceType() ResourceType { 36 | return ResourceTypeFc 37 | } 38 | 39 | func NewResourceFc(res *C.hd_res_t, resType ResourceType) (*ResourceFc, error) { 40 | if res == nil { 41 | return nil, errors.New("res is nil") 42 | } 43 | 44 | if resType != ResourceTypeFc { 45 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeFc, resType) 46 | } 47 | 48 | fc := C.hd_res_get_fc(res) 49 | 50 | return &ResourceFc{ 51 | WwpnOk: bool(C.hd_res_fc_get_wwpn_ok(fc)), 52 | FcpLunOk: bool(C.hd_res_fc_get_fcp_lun_ok(fc)), 53 | PortIDOk: bool(C.hd_res_fc_get_port_id_ok(fc)), 54 | Wwpn: uint64(fc.wwpn), 55 | FcpLun: uint64(fc.fcp_lun), 56 | PortID: uint32(fc.port_id), 57 | ControllerID: byte(*fc.controller_id), 58 | }, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/ephem/ephem.go: -------------------------------------------------------------------------------- 1 | // Package ephem contains utilities for capturing ephemeral aspects of a target machine. 2 | // 3 | // Currently, it only supports capturing swap configurations. 4 | // Eventually it will capture more things, such as filesystems. 5 | package ephem 6 | 7 | import ( 8 | "fmt" 9 | "log/slog" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | var deviceGlobs = []string{ 16 | "/dev/stratis/*/*", 17 | "/dev/disk/by-uuid/*", 18 | "/dev/mapper/*", 19 | "/dev/disk/by-label/*", 20 | } 21 | 22 | // StableDevicePath takes a device path and converts it into a more stable form. 23 | // For example, /dev/nvme* is assigned on startup by detection order which is not consistent. 24 | // A disk path of the form /dev/disk/by-uuid/* is not startup-dependent. 25 | func StableDevicePath(device string) (string, error) { 26 | l := slog.With("prefix", "stableDevicePath") 27 | 28 | if !strings.HasPrefix(device, "/") { 29 | return device, nil 30 | } 31 | 32 | stat, err := os.Stat(device) 33 | if err != nil { 34 | return "", fmt.Errorf("failed to stat device %s: %w", device, err) 35 | } 36 | 37 | for idx := range deviceGlobs { 38 | glob := deviceGlobs[idx] 39 | l.Debug("searching glob", "glob", glob) 40 | 41 | matches, err := filepath.Glob(glob) 42 | if err != nil { 43 | // the only possible error is ErrBadPattern 44 | return "", fmt.Errorf("failed to glob %s: %w", glob, err) 45 | } 46 | 47 | for _, match := range matches { 48 | matchStat, err := os.Stat(match) 49 | if err != nil { 50 | l.Debug("failed to stat match", "match", match, "error", err) 51 | 52 | continue 53 | } 54 | 55 | if os.SameFile(stat, matchStat) { 56 | l.Debug("match found for device", "match", match, "device", device) 57 | 58 | return match, nil 59 | } 60 | } 61 | } 62 | 63 | l.Debug("no match found for device", "device", device) 64 | // if no match was found, we return the original device path 65 | return device, nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/hwinfo/driver_info_display.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | type DriverInfoDisplay struct { 10 | Type DriverInfoType `json:"type,omitempty"` 11 | // actual driver database entries 12 | DBEntry0 []string `json:"db_entry_0,omitempty"` 13 | DBEntry1 []string `json:"db_entry_1,omitempty"` 14 | 15 | Width uint32 `json:"width"` 16 | Height uint32 `json:"height"` 17 | VerticalSync SyncRange `json:"vertical_sync"` 18 | HorizontalSync SyncRange `json:"horizontal_sync"` 19 | Bandwidth uint32 `json:"bandwidth"` 20 | HorizontalSyncTimings SyncTimings `json:"horizontal_sync_timings"` 21 | VerticalSyncTimings SyncTimings `json:"vertical_sync_timings"` 22 | HorizontalFlag byte `json:"horizontal_flag"` 23 | VerticalFlag byte `json:"vertical_flag"` 24 | } 25 | 26 | func (d DriverInfoDisplay) DriverInfoType() DriverInfoType { 27 | return DriverInfoTypeDisplay 28 | } 29 | 30 | func NewDriverInfoDisplay(info C.driver_info_display_t) DriverInfoDisplay { 31 | return DriverInfoDisplay{ 32 | Type: DriverInfoTypeDisplay, 33 | DBEntry0: ReadStringList(info.hddb0), 34 | DBEntry1: ReadStringList(info.hddb1), 35 | Width: uint32(info.width), 36 | Height: uint32(info.height), 37 | VerticalSync: SyncRange{ 38 | Min: uint16(info.min_vsync), 39 | Max: uint16(info.max_vsync), 40 | }, 41 | HorizontalSync: SyncRange{ 42 | Min: uint16(info.min_hsync), 43 | Max: uint16(info.max_hsync), 44 | }, 45 | HorizontalSyncTimings: SyncTimings{ 46 | Disp: uint16(info.hdisp), 47 | SyncStart: uint16(info.hsyncstart), 48 | SyncEnd: uint16(info.hsyncend), 49 | Total: uint32(info.htotal), 50 | }, 51 | VerticalSyncTimings: SyncTimings{ 52 | Disp: uint16(info.vdisp), 53 | SyncStart: uint16(info.vsyncstart), 54 | SyncEnd: uint16(info.vsyncend), 55 | Total: uint32(info.vtotal), 56 | }, 57 | HorizontalFlag: byte(info.hflag), 58 | VerticalFlag: byte(info.vflag), 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_cache.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosCache captures processor information. 10 | type SmbiosCache struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | Socket string `json:"socket"` // socket designation 14 | SizeMax uint32 `json:"size_max"` // max cache size in kbytes 15 | SizeCurrent uint32 `json:"size_current"` // current size in kbytes 16 | Speed uint32 `json:"speed"` // cache speed in nanoseconds 17 | Mode *ID `json:"mode"` // operational mode 18 | Enabled bool `json:"enabled"` 19 | Location *ID `json:"location"` // cache location 20 | Socketed bool `json:"socketed"` 21 | Level uint `json:"level"` // cache level (0 = L1, 1 = L2, ...) 22 | ECC *ID `json:"ecc"` // error correction type 23 | CacheType *ID `json:"cache_type"` // logical cache type 24 | Associativity *ID `json:"associativity"` // cache associativity 25 | SRAMType []string `json:"sram_type_current"` 26 | SRAMTypes []string `json:"sram_type_supported"` 27 | } 28 | 29 | func (s SmbiosCache) SmbiosType() SmbiosType { 30 | return s.Type 31 | } 32 | 33 | func NewSmbiosCache(info C.smbios_cache_t) (*SmbiosCache, error) { 34 | return &SmbiosCache{ 35 | Type: SmbiosTypeCache, 36 | Handle: int(info.handle), 37 | Socket: C.GoString(info.socket), 38 | SizeMax: uint32(info.max_size), 39 | SizeCurrent: uint32(info.current_size), 40 | Speed: uint32(info.speed), 41 | Mode: NewID(info.mode), 42 | Enabled: uint(info.state) == 1, 43 | Location: NewID(info.location), 44 | Socketed: uint(info.socketed) == 1, 45 | Level: uint(info.level), 46 | ECC: NewID(info.ecc), 47 | CacheType: NewID(info.cache_type), 48 | Associativity: NewID(info.assoc), 49 | SRAMType: ReadStringList(info.sram.str), 50 | SRAMTypes: ReadStringList(info.supp_sram.str), 51 | }, nil 52 | } 53 | -------------------------------------------------------------------------------- /scripts/create-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" 6 | cd "$SCRIPT_DIR/.." 7 | 8 | version="${1:-}" 9 | if [[ -z $version ]]; then 10 | echo "USAGE: $0 version" >&2 11 | exit 1 12 | fi 13 | 14 | if [[ "$(git symbolic-ref --short HEAD)" != "main" ]]; then 15 | echo "must be on main branch" >&2 16 | exit 1 17 | fi 18 | 19 | waitForPr() { 20 | local pr=$1 21 | while true; do 22 | if gh pr view "$pr" | grep -q 'MERGED'; then 23 | break 24 | fi 25 | echo "Waiting for PR to be merged..." 26 | sleep 5 27 | done 28 | } 29 | 30 | # ensure we are up-to-date 31 | uncommitted_changes=$(git diff --compact-summary) 32 | if [[ -n $uncommitted_changes ]]; then 33 | echo -e "There are uncommitted changes, exiting:\n${uncommitted_changes}" >&2 34 | exit 1 35 | fi 36 | git pull git@github.com:numtide/nixos-facter main 37 | unpushed_commits=$(git log --format=oneline origin/main..main) 38 | if [[ $unpushed_commits != "" ]]; then 39 | echo -e "\nThere are unpushed changes, exiting:\n$unpushed_commits" >&2 40 | exit 1 41 | fi 42 | # make sure tag does not exist 43 | if git tag -l | grep -q "^v${version}\$"; then 44 | echo "Tag v${version} already exists, exiting" >&2 45 | exit 1 46 | fi 47 | sed -i "s/version = \".*\"/version = \"$version\"/" ./nix/packages/nixos-facter/package.nix 48 | git add ./nix/packages/nixos-facter/package.nix 49 | git branch -D "release-${version}" || true 50 | git checkout -b "release-${version}" 51 | git commit -m "release: v${version}" 52 | nix flake check -vL 53 | git push origin "release-${version}" 54 | pr_url=$(gh pr create \ 55 | --base main \ 56 | --head "release-${version}" \ 57 | --title "Release v${version}" \ 58 | --body "Release v${version} of nixos-facter") 59 | 60 | # Extract PR number from URL 61 | pr_number=$(echo "$pr_url" | grep -oE '[0-9]+$') 62 | 63 | # Enable auto-merge with specific merge method and delete branch 64 | gh pr merge "$pr_number" --auto --merge 65 | git checkout main 66 | 67 | waitForPr "release-${version}" 68 | git pull git@github.com:numtide/nixos-facter main 69 | git tag "v${version}" 70 | git push origin "v${version}" 71 | 72 | echo "Release v${version} complete!" 73 | -------------------------------------------------------------------------------- /docs/content/contributing/code.md: -------------------------------------------------------------------------------- 1 | # Code 2 | 3 | ## Pre-requisites 4 | 5 | You will need to have the following installed: 6 | 7 | - [Nix] 8 | - [Direnv] 9 | 10 | !!! important 11 | 12 | We use a [Flake]-based workflow. You can certainly develop for `nixos-facter` without Flakes and leverage 13 | much of what is listed below, but it is left up to the reader to determine how to make that work. 14 | 15 | ## Go development 16 | 17 | The default [devshell] is a modified version of the underlying [Nix derivation] for `nixos-facter`. As such, it provides 18 | all the necessary tooling and utilities for working on `nixos-facter`. 19 | 20 | ```nix title="nix/devshells/default.nix" 21 | --8<-- "nix/devshells/default.nix" 22 | ``` 23 | 24 | [Direnv] should load this by default when entering the root of the repository. 25 | 26 | For the most part, you _should_ be able to develop normally as you would any other Go program. 27 | 28 | !!! important 29 | 30 | When you have completed making any changes and have tested it as you would any other Go program, it is important 31 | to ensure it works when run as a Nix package. 32 | 33 | This can be done with `nix run .# -- `, which will build the Nix derivation and execute the resultant 34 | `nixos-facter` binary. 35 | 36 | ## Formatting 37 | 38 | We use [treefmt] and [treefmt-nix] to format the repository by running `nix fmt` from the root directory. 39 | 40 | ```nix title="nix/formatter.nix" 41 | --8<-- "nix/formatter.nix" 42 | ``` 43 | 44 | ## Checks 45 | 46 | Running `nix flake check` will build all the devshells and Nix packages, as well as check the formatting with [treefmt] 47 | and any other [Flake checks](https://github.com/NixOS/nix/blob/master/src/nix/flake-check.md) that have been configured. 48 | 49 | ## Documentation 50 | 51 | When making changes, it is **important** to add or update any relevant sections in the documentation within the same 52 | pull request. 53 | 54 | For more information see the [next section](./docs.md). 55 | 56 | [Nix]: https://nixos.org 57 | [Flake]: https://wiki.nixos.org/wiki/Flakes 58 | [Nix derivation]: https://nix.dev/manual/nix/2.18/language/derivations 59 | [Direnv]: https://direnv.net 60 | [devshell]: https://nix.dev/tutorials/first-steps/declarative-shell.html 61 | [treefmt]: https://treefmt.com 62 | [treefmt-nix]: https://github.com/numtide/treefmt-nix 63 | -------------------------------------------------------------------------------- /pkg/hwinfo/detail_bios.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | bool bios_info_is_apm_supported(bios_info_t *info) { return info->apm_supported; } 9 | bool bios_info_is_apm_enabled(bios_info_t *info) { return info->apm_enabled; } 10 | bool bios_info_is_pnp_bios(bios_info_t *info) { return info->is_pnp_bios; } 11 | bool bios_info_has_lba_support(bios_info_t *info) { return info->lba_support; } 12 | */ 13 | import "C" 14 | 15 | type ApmInfo struct { 16 | Supported bool `json:"supported"` 17 | Enabled bool `json:"enabled"` 18 | Version uint8 `json:"version"` 19 | SubVersion uint8 `json:"sub_version"` 20 | BiosFlags uint32 `json:"bios_flags"` 21 | } 22 | 23 | type VbeInfo struct { 24 | Version uint16 `json:"version"` 25 | VideoMemory uint32 `json:"video_memory"` 26 | } 27 | 28 | type DetailBios struct { 29 | Type DetailType `json:"-"` 30 | ApmInfo ApmInfo `json:"apm_info"` 31 | VbeInfo VbeInfo `json:"vbe_info"` 32 | 33 | // todo par and ser ports 34 | PnP bool `json:"pnp"` 35 | PnPId uint `json:"pnp_id"` // it is still in big endian format 36 | LbaSupport bool `json:"lba_support"` 37 | LowMemorySize uint32 `json:"low_memory_size"` 38 | // todo smp info 39 | // todo vbe info 40 | 41 | SmbiosVersion uint32 `json:"smbios_version"` 42 | 43 | // todo lcd 44 | // todo mouse 45 | // todo led 46 | // todo bios32 47 | } 48 | 49 | func (d DetailBios) DetailType() DetailType { 50 | return DetailTypeBios 51 | } 52 | 53 | func NewDetailBios(dev C.hd_detail_bios_t) (*DetailBios, error) { 54 | data := dev.data 55 | 56 | return &DetailBios{ 57 | Type: DetailTypeBios, 58 | ApmInfo: ApmInfo{ 59 | Supported: bool(C.bios_info_is_apm_supported(data)), 60 | Enabled: bool(C.bios_info_is_apm_enabled(data)), 61 | Version: uint8(data.apm_ver), 62 | SubVersion: uint8(data.apm_subver), 63 | BiosFlags: uint32(data.apm_bios_flags), 64 | }, 65 | VbeInfo: VbeInfo{ 66 | Version: uint16(data.vbe_ver), 67 | VideoMemory: uint32(data.vbe_video_mem), 68 | }, 69 | PnP: bool(C.bios_info_is_pnp_bios(data)), 70 | PnPId: uint(data.pnp_id), 71 | LbaSupport: bool(C.bios_info_has_lba_support(data)), 72 | LowMemorySize: uint32(data.low_mem_size), 73 | SmbiosVersion: uint32(data.smbios_ver), 74 | }, nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/ephem/swap_test.go: -------------------------------------------------------------------------------- 1 | package ephem_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/numtide/nixos-facter/pkg/ephem" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | var ( 12 | empty = "Filename Type Size Used Priority\n" 13 | corrupt = `Filename Type Size Used Priority 14 | foo bar baz 15 | ` 16 | badPartition = `Filename Type Size Used Priority 17 | /var/lib/swap-1 foo 1048576 123 -3 18 | ` 19 | 20 | sample = `Filename Type Size Used Priority 21 | /dev/sda6 partition 4194300 0 -1 22 | /var/lib/swap-1 file 1048576 123 -3 23 | /var/lib/swap-2 file 2097152 4567 -2` 24 | ) 25 | 26 | func TestReadSwapFile(t *testing.T) { 27 | t.Parallel() 28 | as := require.New(t) 29 | 30 | _, err := ephem.ReadSwapFile(strings.NewReader("")) 31 | as.Error(err, "swaps file is empty") 32 | 33 | _, err = ephem.ReadSwapFile(strings.NewReader("foo bar baz hello world\n")) 34 | as.Errorf(err, "header in swaps file is malformed: '%s'", "foo bar baz hello world\n") 35 | 36 | swaps, err := ephem.ReadSwapFile(strings.NewReader(empty)) 37 | as.NoError(err) 38 | as.Empty(swaps) 39 | 40 | _, err = ephem.ReadSwapFile(strings.NewReader(corrupt)) 41 | as.Errorf(err, "malformed entry in swaps file: '%s'", "foo bar baz") 42 | 43 | _, err = ephem.ReadSwapFile(strings.NewReader(badPartition)) 44 | as.Errorf(err, "malformed entry in swaps file: '%s'", `/var/lib/swap-1 foo 1048576 123 -3`) 45 | 46 | swaps, err = ephem.ReadSwapFile(strings.NewReader(sample)) 47 | as.NoError(err) 48 | as.Len(swaps, 3) 49 | 50 | as.Equal("/dev/sda6", swaps[0].Filename) 51 | as.Equal(ephem.SwapTypePartition, swaps[0].Type) 52 | as.Equal(uint64(4194300), swaps[0].Size) 53 | as.Equal(uint64(0), swaps[0].Used) 54 | as.Equal(int32(-1), swaps[0].Priority) 55 | 56 | as.Equal("/var/lib/swap-1", swaps[1].Filename) 57 | as.Equal(ephem.SwapTypeFile, swaps[1].Type) 58 | as.Equal(uint64(1048576), swaps[1].Size) 59 | as.Equal(uint64(123), swaps[1].Used) 60 | as.Equal(int32(-3), swaps[1].Priority) 61 | 62 | as.Equal("/var/lib/swap-2", swaps[2].Filename) 63 | as.Equal(ephem.SwapTypeFile, swaps[2].Type) 64 | as.Equal(uint64(2097152), swaps[2].Size) 65 | as.Equal(uint64(4567), swaps[2].Used) 66 | as.Equal(int32(-2), swaps[2].Priority) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_mem.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool hd_res_mem_get_enabled(res_mem_t res) { return res.enabled; } 10 | unsigned hd_res_mem_get_access(res_mem_t res) { return res.access; } 11 | unsigned hd_res_mem_get_prefetch(res_mem_t res) { return res.prefetch; } 12 | 13 | // CGO cannot access union type fields, so we do this as a workaround 14 | res_mem_t hd_res_get_mem(hd_res_t *res) { return res->mem; } 15 | 16 | */ 17 | import "C" 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | ) 23 | 24 | //go:generate enumer -type=AccessFlags -json -transform=snake -trimprefix AccessFlags -output=./resource_enum_access_flags.go 25 | type AccessFlags uint //nolint:recvcheck 26 | 27 | const ( 28 | AccessFlagsUnknown AccessFlags = iota 29 | AccessFlagsReadOnly 30 | AccessFlagsWriteOnly 31 | AccessFlagsReadWrite 32 | ) 33 | 34 | //go:generate enumer -type=YesNoFlags -json -transform=snake -trimprefix YesNoFlags -output=./resource_enum_yes_no_flags.go 35 | type YesNoFlags uint //nolint:recvcheck 36 | 37 | const ( 38 | YesNoFlagsUnknown YesNoFlags = iota 39 | YesNoFlagsNo 40 | YesNoFlagsYes 41 | ) 42 | 43 | type Resource interface { 44 | ResourceType() ResourceType 45 | } 46 | 47 | type ResourceMemory struct { 48 | Type ResourceType `json:"type"` 49 | Base uint64 `json:"base"` 50 | Range uint64 `json:"range"` 51 | Enabled bool `json:"enabled"` 52 | Access AccessFlags `json:"access"` 53 | Prefetch YesNoFlags `json:"prefetch"` 54 | } 55 | 56 | func (r ResourceMemory) ResourceType() ResourceType { 57 | return ResourceTypeMem 58 | } 59 | 60 | func NewResourceMemory(res *C.hd_res_t, resType ResourceType) (*ResourceMemory, error) { 61 | if res == nil { 62 | return nil, errors.New("res is nil") 63 | } 64 | 65 | if resType != ResourceTypeMem { 66 | return nil, fmt.Errorf("expected resource type '%s', found '%s'", ResourceTypeMem, resType) 67 | } 68 | 69 | mem := C.hd_res_get_mem(res) 70 | 71 | return &ResourceMemory{ 72 | Type: resType, 73 | Base: uint64(mem.base), 74 | Range: uint64(mem._range), 75 | Enabled: bool(C.hd_res_mem_get_enabled(mem)), 76 | Access: AccessFlags(C.hd_res_mem_get_access(mem)), 77 | Prefetch: YesNoFlags(C.hd_res_mem_get_prefetch(mem)), 78 | }, nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/hwinfo/id.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "slices" 13 | ) 14 | 15 | //go:generate enumer -type=IDTag -json -transform=snake -trimprefix IDTag -output=./id_tag_enum.go 16 | type IDTag byte //nolint:recvcheck 17 | 18 | const ( 19 | IDTagPci IDTag = iota + 1 20 | IDTagEisa 21 | IDTagUsb 22 | IDTagSpecial 23 | IDTagPcmcia 24 | IDTagSdio 25 | ) 26 | 27 | type ID struct { 28 | Type IDTag 29 | Value uint16 30 | // Name (if any) 31 | Name string 32 | } 33 | 34 | type idJSON struct { 35 | Hex string `json:"hex,omitempty"` 36 | Name string `json:"name,omitempty"` 37 | Value uint16 `json:"value"` 38 | } 39 | 40 | func (i ID) MarshalJSON() ([]byte, error) { 41 | var ( 42 | b []byte 43 | err error 44 | ) 45 | 46 | switch i.Type { 47 | case IDTagSpecial: 48 | b, err = json.Marshal(i.Name) 49 | 50 | case 0, IDTagPci, IDTagEisa, IDTagUsb, IDTagPcmcia, IDTagSdio: 51 | b, err = json.Marshal(idJSON{ 52 | Hex: fmt.Sprintf("%04x", i.Value), 53 | Name: i.Name, 54 | Value: i.Value, 55 | }) 56 | 57 | default: 58 | err = fmt.Errorf("unknown id type %d", i.Type) 59 | } 60 | 61 | if err != nil { 62 | err = fmt.Errorf("failed to marshal id %s: %w", i, err) 63 | } 64 | 65 | return b, err 66 | } 67 | 68 | func (i ID) IsEmpty() bool { 69 | return i.Type == 0 && i.Value == 0 && (i.Name == "" || i.Name == "None") 70 | } 71 | 72 | func (i ID) String() string { 73 | return fmt.Sprintf("%d:%s", i.Value, i.Name) 74 | } 75 | 76 | func (i ID) Is(ids ...uint16) bool { 77 | return slices.Contains(ids, i.Value) 78 | } 79 | 80 | func NewID(id C.hd_id_t) *ID { 81 | result := ID{ 82 | /* 83 | ID is actually a combination of some tag to differentiate the various id types and the real id. 84 | We do the same thing as the ID_VALUE macro in hd.h to get the true value. 85 | */ 86 | Type: IDTag((id.id >> 16) & 0xf), 87 | Value: uint16(id.id), 88 | Name: C.GoString(id.name), 89 | } 90 | if result.IsEmpty() { 91 | return nil 92 | } 93 | 94 | return &result 95 | } 96 | 97 | func NewBusID(bus Bus) *ID { 98 | return &ID{ 99 | Name: bus.String(), 100 | Value: uint16(bus), 101 | } 102 | } 103 | 104 | func NewBaseClassID(bc BaseClass) *ID { 105 | return &ID{ 106 | Name: bc.String(), 107 | Value: uint16(bc), 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /pkg/hwinfo/driver_info.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | enum driver_info_type driver_info_get_type(driver_info_t *info) { return info->any.type; } 8 | driver_info_module_t driver_info_get_module(driver_info_t *info) { return info->module; } 9 | driver_info_mouse_t driver_info_get_mouse(driver_info_t *info) { return info->mouse; } 10 | driver_info_display_t driver_info_get_display(driver_info_t *info) { return info->display; } 11 | driver_info_kbd_t driver_info_get_kbd(driver_info_t *info) { return info->kbd; } 12 | driver_info_dsl_t driver_info_get_dsl(driver_info_t *info) { return info->dsl; } 13 | driver_info_isdn_t driver_info_get_isdn(driver_info_t *info) { return info->isdn; } 14 | driver_info_x11_t driver_info_get_x11(driver_info_t *info) { return info->x11; } 15 | */ 16 | import "C" 17 | 18 | import ( 19 | "errors" 20 | ) 21 | 22 | //go:generate enumer -type=DriverInfoType -json -transform=snake -trimprefix DriverInfoType -output=./driver_info_enum_type.go 23 | type DriverInfoType uint //nolint:recvcheck 24 | 25 | const ( 26 | DriverInfoTypeAny DriverInfoType = iota 27 | DriverInfoTypeDisplay 28 | DriverInfoTypeModule 29 | DriverInfoTypeMouse 30 | DriverInfoTypeX11 31 | DriverInfoTypeIsdn 32 | DriverInfoTypeKeyboard 33 | DriverInfoTypeDsl 34 | ) 35 | 36 | type DriverInfo interface { 37 | DriverInfoType() DriverInfoType 38 | } 39 | 40 | //nolint:ireturn 41 | func NewDriverInfo(info *C.driver_info_t) (DriverInfo, error) { 42 | if info == nil { 43 | return nil, errors.New("info is nil") 44 | } 45 | 46 | var ( 47 | err error 48 | result DriverInfo 49 | ) 50 | 51 | switch DriverInfoType(C.driver_info_get_type(info)) { 52 | case DriverInfoTypeModule: 53 | result, err = NewDriverInfoModule(C.driver_info_get_module(info)), nil 54 | case DriverInfoTypeMouse: 55 | result, err = NewDriverInfoMouse(C.driver_info_get_mouse(info)), nil 56 | case DriverInfoTypeDisplay: 57 | result, err = NewDriverInfoDisplay(C.driver_info_get_display(info)), nil 58 | case DriverInfoTypeKeyboard: 59 | result, err = NewDriverInfoKeyboard(C.driver_info_get_kbd(info)), nil 60 | case DriverInfoTypeDsl: 61 | result, err = NewDriverInfoDsl(C.driver_info_get_dsl(info)), nil 62 | case DriverInfoTypeIsdn: 63 | result, err = NewDriverInfoIsdn(C.driver_info_get_isdn(info)), nil 64 | case DriverInfoTypeX11: 65 | result, err = NewDriverInfoX11(C.driver_info_get_x11(info)), nil 66 | case DriverInfoTypeAny: 67 | err = errors.New("unknown driver info type") 68 | } 69 | 70 | return result, err 71 | } 72 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_memory_device.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosMemoryDevice captures system slot information. 10 | // 11 | //nolint:lll 12 | type SmbiosMemoryDevice struct { 13 | Type SmbiosType `json:"-"` 14 | Handle int `json:"handle"` 15 | Location string `json:"location"` // device location 16 | BankLocation string `json:"bank_location"` // bank location 17 | Manufacturer string `json:"manufacturer"` 18 | Serial string `json:"-"` // omit from json 19 | AssetTag string `json:"-"` 20 | PartNumber string `json:"part_number"` 21 | ArrayHandle int `json:"array_handle"` // memory array this device belongs to 22 | ErrorHandle int `json:"error_handle"` // points to error info record; 0xfffe: not supported, 0xffff: no error 23 | Width uint16 `json:"width"` // data width in bits 24 | ECCBits uint8 `json:"ecc_bits"` // ecc bits 25 | Size uint `json:"size"` // kB 26 | FormFactor *ID `json:"form_factor"` 27 | Set uint8 `json:"set"` // 0: does not belong to a set; 1-0xfe: set number; 0xff: unknown 28 | MemoryType *ID `json:"memory_type"` 29 | MemoryTypeDetails []string `json:"memory_type_details"` 30 | Speed uint32 `json:"speed"` // MHz 31 | } 32 | 33 | func (s SmbiosMemoryDevice) SmbiosType() SmbiosType { 34 | return s.Type 35 | } 36 | 37 | func NewSmbiosMemDevice(info C.smbios_memdevice_t) (*SmbiosMemoryDevice, error) { 38 | return &SmbiosMemoryDevice{ 39 | Type: SmbiosTypeMemoryDevice, 40 | Handle: int(info.handle), 41 | Location: C.GoString(info.location), 42 | BankLocation: C.GoString(info.bank), 43 | Manufacturer: C.GoString(info.manuf), 44 | Serial: C.GoString(info.serial), 45 | AssetTag: C.GoString(info.asset), 46 | PartNumber: C.GoString(info.part), 47 | ArrayHandle: int(info.array_handle), 48 | ErrorHandle: int(info.error_handle), 49 | Width: uint16(info.width), 50 | ECCBits: uint8(info.eccbits), 51 | Size: uint(info.size), 52 | FormFactor: NewID(info.form), 53 | Set: uint8(info.set), 54 | MemoryType: NewID(info.mem_type), 55 | MemoryTypeDetails: ReadStringList(info.type_detail.str), 56 | Speed: uint32(info.speed), 57 | }, nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/hwinfo/smbios_processor.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | // SmbiosProcessor captures processor information. 10 | type SmbiosProcessor struct { 11 | Type SmbiosType `json:"-"` 12 | Handle int `json:"handle"` 13 | Socket string `json:"socket"` 14 | SocketType *ID `json:"socket_type"` 15 | SocketPopulated bool `json:"socket_populated"` // true: populated, false: empty 16 | Manufacturer string `json:"manufacturer"` 17 | Version string `json:"version"` 18 | Serial string `json:"-"` // omit from json output 19 | AssetTag string `json:"-"` // asset tag 20 | Part string `json:"part"` // part number 21 | ProcessorType *ID `json:"processor_type"` 22 | ProcessorFamily *ID `json:"processor_family"` 23 | ProcessorID uint64 `json:"-"` // omit from json 24 | ProcessorStatus *ID `json:"processor_status"` 25 | Voltage uint `json:"-"` 26 | ClockExt uint32 `json:"clock_ext"` // MHz 27 | ClockMax uint32 `json:"clock_max"` // MHz 28 | ClockCurrent uint `json:"-"` // MHz 29 | CacheHandleL1 int `json:"cache_handle_l1"` // handle of L1 cache 30 | CacheHandleL2 int `json:"cache_handle_l2"` // handle of L2 cache 31 | CacheHandleL3 int `json:"cache_handle_l3"` // handle of L3 cache 32 | } 33 | 34 | func (s SmbiosProcessor) SmbiosType() SmbiosType { 35 | return s.Type 36 | } 37 | 38 | func NewSmbiosProcessor(info C.smbios_processor_t) (*SmbiosProcessor, error) { 39 | return &SmbiosProcessor{ 40 | Type: SmbiosTypeProcessor, 41 | Handle: int(info.handle), 42 | Socket: C.GoString(info.socket), 43 | SocketType: NewID(info.upgrade), 44 | SocketPopulated: uint(info.sock_status) == 1, 45 | Manufacturer: C.GoString(info.manuf), 46 | Version: C.GoString(info.version), 47 | Serial: C.GoString(info.serial), 48 | AssetTag: C.GoString(info.asset), 49 | Part: C.GoString(info.part), 50 | ProcessorType: NewID(info.pr_type), 51 | ProcessorFamily: NewID(info.family), 52 | ProcessorID: uint64(info.cpu_id), 53 | ProcessorStatus: NewID(info.cpu_status), 54 | Voltage: uint(info.voltage), 55 | ClockExt: uint32(info.ext_clock), 56 | ClockMax: uint32(info.max_speed), 57 | ClockCurrent: uint(info.current_speed), 58 | CacheHandleL1: int(info.l1_cache), 59 | CacheHandleL2: int(info.l2_cache), 60 | CacheHandleL3: int(info.l3_cache), 61 | }, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/hwinfo/detail_isa_pnp_dev.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool hd_isapnp_card_get_broken(isapnp_card_t *card) { return card->broken; } 10 | */ 11 | import "C" 12 | 13 | import ( 14 | "encoding/hex" 15 | "errors" 16 | "unsafe" 17 | ) 18 | 19 | type IsaPnpResource struct { 20 | Length int `json:"length"` 21 | Type int `json:"type"` 22 | Data string `json:"-"` // hex encoded 23 | } 24 | 25 | func NewIsaPnpResource(res *C.isapnp_res_t) *IsaPnpResource { 26 | if res == nil { 27 | return nil 28 | } 29 | 30 | return &IsaPnpResource{ 31 | Length: int(res.len), 32 | Type: int(res._type), 33 | Data: hex.EncodeToString(C.GoBytes(unsafe.Pointer(&res.data), res.len)), //nolint:gocritic 34 | } 35 | } 36 | 37 | type IsaPnpCard struct { 38 | Csn int `json:"csn"` 39 | LogDevs int `json:"log_devs"` // todo full name? 40 | Serial string `json:"-"` 41 | CardRegs string `json:"card_regs"` // todo full name? 42 | LdevRegs string `json:"ldev_regs"` // todo full name? hex encoded 43 | ResLen int `json:"res_len"` // todo full name? 44 | Broken bool `json:"broken"` // mark a broken card 45 | Resource *IsaPnpResource `json:"resource"` 46 | } 47 | 48 | func NewIsaPnpCard(card *C.isapnp_card_t) (*IsaPnpCard, error) { 49 | if card == nil { 50 | return nil, errors.New("card is nil") 51 | } 52 | 53 | return &IsaPnpCard{ 54 | Csn: int(card.csn), 55 | LogDevs: int(card.log_devs), 56 | // Serial: C.GoString(card.serial), todo 57 | // CardRegs: C.GoString(card.card_regs), todo 58 | LdevRegs: hex.EncodeToString(C.GoBytes(unsafe.Pointer(&card.ldev_regs), C.int(0xd0))), //nolint:gocritic 59 | ResLen: int(card.res_len), 60 | Broken: bool(C.hd_isapnp_card_get_broken(card)), 61 | Resource: NewIsaPnpResource(card.res), 62 | }, nil 63 | } 64 | 65 | type DetailIsaPnpDevice struct { 66 | Type DetailType `json:"-"` 67 | Card *IsaPnpCard `json:"card"` 68 | Device uint32 `json:"device"` 69 | Flags uint32 `json:"flags"` 70 | } 71 | 72 | func (d DetailIsaPnpDevice) DetailType() DetailType { 73 | return DetailTypeIsaPnp 74 | } 75 | 76 | func NewDetailIsaPnpDevice(pnp C.hd_detail_isapnp_t) (*DetailIsaPnpDevice, error) { 77 | data := pnp.data 78 | 79 | card, err := NewIsaPnpCard(data.card) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return &DetailIsaPnpDevice{ 85 | Type: DetailTypeIsaPnp, 86 | Card: card, 87 | Device: uint32(data.dev), 88 | Flags: uint32(data.flags), 89 | }, nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/hwinfo/hwinfo.go: -------------------------------------------------------------------------------- 1 | // Package hwinfo provides functionality for scanning hardware devices and reading SMBIOS information. 2 | // It does this using the [hwinfo] C library. 3 | // 4 | // [hwinfo]: https://github.com/numtide/hwinfo 5 | package hwinfo 6 | 7 | /* 8 | #cgo pkg-config: hwinfo 9 | #include 10 | #include 11 | 12 | // CGO cannot access union type fields, so we do this as a workaround 13 | hd_smbios_t* hd_smbios_next(hd_smbios_t *sm) { return sm->next; } 14 | */ 15 | import "C" 16 | 17 | import ( 18 | "unsafe" 19 | ) 20 | 21 | func excludeDevice(item *HardwareDevice) bool { 22 | if item.Class == HardwareClassNetworkInterface { 23 | for _, driver := range item.Drivers { 24 | // devices that are not mapped to hardware should be not included in the hardware report 25 | if virtualNetworkDevices[driver] { 26 | return true 27 | } 28 | } 29 | } 30 | 31 | return false 32 | } 33 | 34 | // Scan returns a list of SMBIOS entries and detected hardware devices based on the provided probe features. 35 | func Scan(probes []ProbeFeature, ephemeral bool) ([]Smbios, []HardwareDevice, error) { 36 | // initialise the struct to hold scan data 37 | data := (*C.hd_data_t)(C.calloc(1, C.size_t(unsafe.Sizeof(C.hd_data_t{})))) 38 | 39 | // ProbeFeatureInt needs to always be set, otherwise we don't get pci and usb vendor id lookups. 40 | // https://github.com/openSUSE/hwinfo/blob/c87f449f1d4882c71b0a1e6dc80638224a5baeed/src/hd/hd.c#L597-L605 41 | C.hd_set_probe_feature(data, C.enum_probe_feature(ProbeFeatureInt)) 42 | 43 | // set the hardware probes to run 44 | for _, probe := range probes { 45 | C.hd_set_probe_feature(data, C.enum_probe_feature(probe)) 46 | } 47 | 48 | // scan 49 | C.hd_scan(data) 50 | 51 | defer C.hd_free_hd_data(data) 52 | 53 | var smbiosItems []Smbios 54 | 55 | for sm := data.smbios; sm != nil; sm = C.hd_smbios_next(sm) { 56 | item, err := NewSmbios(sm) 57 | if err != nil { 58 | return nil, nil, err 59 | } else if item == nil { 60 | continue 61 | } 62 | 63 | smbiosItems = append(smbiosItems, item) 64 | } 65 | 66 | var ( 67 | deviceIdx uint16 68 | hardwareItems []HardwareDevice 69 | ) 70 | 71 | for hd := data.hd; hd != nil; hd = hd.next { 72 | item, err := NewHardwareDevice(hd, ephemeral) 73 | if err != nil { 74 | return nil, nil, err 75 | } 76 | 77 | if item.Index > deviceIdx { 78 | deviceIdx = item.Index 79 | } 80 | 81 | if excludeDevice(item) { 82 | continue 83 | } 84 | 85 | hardwareItems = append(hardwareItems, *item) 86 | } 87 | 88 | // probe for additional inputs that hwinfo does not support 89 | touchpads, err := captureTouchpads(deviceIdx + 1) 90 | if err != nil { 91 | return nil, nil, err 92 | } 93 | 94 | hardwareItems = append(hardwareItems, touchpads...) 95 | 96 | return smbiosItems, hardwareItems, nil 97 | } 98 | -------------------------------------------------------------------------------- /docs/content/getting-started/generate-report.md: -------------------------------------------------------------------------------- 1 | # Generate a report 2 | 3 | To generate a report, you will need to have [Nix] installed on the target machine. 4 | 5 | === "Nixpkgs" 6 | 7 | ```shell 8 | sudo nix run nixpkgs#nixos-facter -- -o facter.json 9 | ``` 10 | 11 | === "Flake" 12 | 13 | ```shell 14 | sudo nix run \ 15 | --option experimental-features "nix-command flakes" \ 16 | --option extra-substituters https://numtide.cachix.org \ 17 | --option extra-trusted-public-keys numtide.cachix.org-1:2ps1kLBUWjxIneOy1Ik6cQjb41X0iXVXeHigGmycPPE= \ 18 | github:nix-community/nixos-facter -- -o facter.json 19 | ``` 20 | 21 | This will scan your system and produce a JSON-based report in a file named `facter.json`: 22 | 23 | ```json title="facter.json" 24 | { 25 | "version": 2, // (1)! 26 | "system": "x86_64-linux", // (2)! 27 | "virtualisation": "none", // (3)! 28 | "hardware": { // (4)! 29 | "bios": { ... }, 30 | "bluetooth": [ ... ], 31 | "bridge": [ ... ], 32 | "chip_card": [ ... ] , 33 | "cpu": [ ... ], 34 | "disk": [ ... ], 35 | "graphics_card": [ ... ], 36 | "hub": [ ... ], 37 | "keyboard": [ ... ], 38 | "memory": [ ... ], 39 | "monitor": [ ... ], 40 | "mouse": [ ... ], 41 | "network_controller": [ ... ], 42 | "network_interface": [ ... ], 43 | "sound": [ ... ], 44 | "storage_controller": [ ... ], 45 | "system": [ ... ], 46 | "unknown": [ ... ], 47 | "usb_controller": [ ... ] 48 | }, 49 | "smbios": { // (5)! 50 | "bios": { ... }, 51 | "board": { ... }, 52 | "cache": [ ... ], 53 | "chassis": { ... }, 54 | "config": { ... }, 55 | "language": { ... }, 56 | "memory_array": [ ... ], 57 | "memory_array_mapped_address": [ ... ], 58 | "memory_device": [ ... ], 59 | "memory_device_mapped_address": [ ... ], 60 | "memory_error": [ ... ], 61 | "onboard": [ ... ], 62 | "port_connector": [ ... ], 63 | "processor": [ ... ], 64 | "slot": [ ... ], 65 | "system": { ... } 66 | } 67 | } 68 | ``` 69 | 70 | 1. Used to track major breaking changes in the report format. 71 | 2. Architecture of the target machine. 72 | 3. Indicates whether the report was generated inside a virtualised environment, and if so, what type. 73 | 4. All the various bits of hardware that could be detected. 74 | 5. [System Management BIOS] information if available. 75 | 76 | !!! tip 77 | 78 | To use this report in your NixOS configuration, have a look at [NixOS Facter Modules]. 79 | 80 | [Nix]: https://nixos.org 81 | [Numtide]: https://numtide.com 82 | [Numtide Binary Cache]: https://numtide.cachix.org 83 | [nixos-facter]: https://github.com/nix-community/nixos-facter 84 | [nixpkgs]: https://github.com/nixos/nixpkgs 85 | [System Management BIOS]: https://wiki.osdev.org/System_Management_BIOS 86 | [NixOS Facter Modules]: https://github.com/nix-community/nixos-facter-modules 87 | -------------------------------------------------------------------------------- /pkg/ephem/swap_enum_type.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=SwapType -json -transform=snake -trimprefix SwapType -output=./swap_enum_type.go"; DO NOT EDIT. 2 | 3 | package ephem 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _SwapTypeName = "filepartition" 12 | 13 | var _SwapTypeIndex = [...]uint8{0, 4, 13} 14 | 15 | const _SwapTypeLowerName = "filepartition" 16 | 17 | func (i SwapType) String() string { 18 | if i >= SwapType(len(_SwapTypeIndex)-1) { 19 | return fmt.Sprintf("SwapType(%d)", i) 20 | } 21 | return _SwapTypeName[_SwapTypeIndex[i]:_SwapTypeIndex[i+1]] 22 | } 23 | 24 | // An "invalid array index" compiler error signifies that the constant values have changed. 25 | // Re-run the stringer command to generate them again. 26 | func _SwapTypeNoOp() { 27 | var x [1]struct{} 28 | _ = x[SwapTypeFile-(0)] 29 | _ = x[SwapTypePartition-(1)] 30 | } 31 | 32 | var _SwapTypeValues = []SwapType{SwapTypeFile, SwapTypePartition} 33 | 34 | var _SwapTypeNameToValueMap = map[string]SwapType{ 35 | _SwapTypeName[0:4]: SwapTypeFile, 36 | _SwapTypeLowerName[0:4]: SwapTypeFile, 37 | _SwapTypeName[4:13]: SwapTypePartition, 38 | _SwapTypeLowerName[4:13]: SwapTypePartition, 39 | } 40 | 41 | var _SwapTypeNames = []string{ 42 | _SwapTypeName[0:4], 43 | _SwapTypeName[4:13], 44 | } 45 | 46 | // SwapTypeString retrieves an enum value from the enum constants string name. 47 | // Throws an error if the param is not part of the enum. 48 | func SwapTypeString(s string) (SwapType, error) { 49 | if val, ok := _SwapTypeNameToValueMap[s]; ok { 50 | return val, nil 51 | } 52 | 53 | if val, ok := _SwapTypeNameToValueMap[strings.ToLower(s)]; ok { 54 | return val, nil 55 | } 56 | return 0, fmt.Errorf("%s does not belong to SwapType values", s) 57 | } 58 | 59 | // SwapTypeValues returns all values of the enum 60 | func SwapTypeValues() []SwapType { 61 | return _SwapTypeValues 62 | } 63 | 64 | // SwapTypeStrings returns a slice of all String values of the enum 65 | func SwapTypeStrings() []string { 66 | strs := make([]string, len(_SwapTypeNames)) 67 | copy(strs, _SwapTypeNames) 68 | return strs 69 | } 70 | 71 | // IsASwapType returns "true" if the value is listed in the enum definition. "false" otherwise 72 | func (i SwapType) IsASwapType() bool { 73 | for _, v := range _SwapTypeValues { 74 | if i == v { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | 81 | // MarshalJSON implements the json.Marshaler interface for SwapType 82 | func (i SwapType) MarshalJSON() ([]byte, error) { 83 | return json.Marshal(i.String()) 84 | } 85 | 86 | // UnmarshalJSON implements the json.Unmarshaler interface for SwapType 87 | func (i *SwapType) UnmarshalJSON(data []byte) error { 88 | var s string 89 | if err := json.Unmarshal(data, &s); err != nil { 90 | return fmt.Errorf("SwapType should be a string, got %s", data) 91 | } 92 | 93 | var err error 94 | *i, err = SwapTypeString(s) 95 | return err 96 | } 97 | -------------------------------------------------------------------------------- /docs/content/contributing/docs.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | There is a separate [devshell] called `docs` which is provided for working with the docs locally. 4 | 5 | It can be entered by running: `nix develop .#docs` 6 | 7 | ```nix title="nix/devshells/docs.nix" 8 | --8<-- "nix/devshells/docs.nix" 9 | ``` 10 | 11 | The docs are based on [MkDocs] and the [MkDocs Material] theme. 12 | You will find its configuration and content in the following locations: 13 | 14 | - `mkdocs.yaml` 15 | - `./docs` 16 | 17 | ## Serve locally 18 | 19 | To serve the docs locally run `mkdocs serve` from the root of the repository: 20 | 21 | ```console 22 | ❯ mkdocs serve 23 | INFO - Building documentation... 24 | INFO - Cleaning site directory 25 | WARNING - The following pages exist in the docs directory, but are not included in the "nav" configuration: 26 | - index.md 27 | INFO - Documentation built in 0.26 seconds 28 | INFO - [16:22:36] Watching paths for changes: 'docs/content', 'mkdocs.yml' 29 | INFO - [16:22:36] Serving on http://127.0.0.1:8000/nixos-facter/ 30 | ``` 31 | 32 | ## Versioning & Publication 33 | 34 | Versioning of the docs is managed through [mike]. 35 | 36 | It is responsible for managing the structure of the `gh-pages` branch in the repository, which [Github Pages] is 37 | configured to serve from. 38 | 39 | !!! note 40 | 41 | More information about versioning with [MkDocs Material] and [mike] can be found [here](https://squidfunk.github.io/mkdocs-material/setup/setting-up-versioning/). 42 | 43 | There is a github workflow, `.github/workflows/gh-pages.yml` which is responsible for publishing the docs. 44 | It does the following: 45 | 46 | - On merge to `main`, the docs version [main](https://numtide.github.io/nixos-facter/main/) is updated. 47 | - When a new tag is created of the form `v...` a docs version `v.` is created and the 48 | [latest](https://numtide.github.io/nixos-facter/latest) alias is updated to point to this. 49 | 50 | The idea is that users will land on the latest released version of the docs by default, with `main` being available if 51 | they wish to read about unreleased features and changes. 52 | 53 | To preview the versions locally you can use `mike serve` instead of `mkdocs serve`. 54 | 55 | !!! warning 56 | 57 | Be sure to have fetched the latest changes for the `gh-pages` branch first. 58 | This is especially important if you are using `mike` locally to make manual changes to the published site. 59 | 60 | [Nix]: https://nixos.org 61 | [Flake]: https://wiki.nixos.org/wiki/Flakes 62 | [Nix derivation]: https://nix.dev/manual/nix/2.18/language/derivations 63 | [Direnv]: https://direnv.net 64 | [devshell]: https://nix.dev/tutorials/first-steps/declarative-shell.html 65 | [MkDocs]: https://www.mkdocs.org/ 66 | [MkDocs Material]: https://squidfunk.github.io/mkdocs-material/ 67 | [Github Pages]: https://pages.github.com/ 68 | [mike]: https://github.com/jimporter/mike 69 | -------------------------------------------------------------------------------- /pkg/hwinfo/detail_monitor.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | type SyncRange struct { 10 | Min uint16 `json:"min"` 11 | Max uint16 `json:"max"` 12 | } 13 | 14 | type SyncTimings struct { 15 | Disp uint16 `json:"disp"` // todo what's the proper name for this? 16 | SyncStart uint16 `json:"sync_start"` 17 | SyncEnd uint16 `json:"sync_end"` 18 | Total uint32 `json:"total"` // todo what's a better name for this? 19 | } 20 | 21 | type DetailMonitor struct { 22 | Type DetailType `json:"-"` 23 | ManufactureYear uint16 `json:"manufacture_year"` 24 | ManufactureWeek uint8 `json:"manufacture_week"` 25 | VerticalSync SyncRange `json:"vertical_sync"` 26 | HorizontalSync SyncRange `json:"horizontal_sync"` 27 | HorizontalSyncTimings SyncTimings `json:"horizontal_sync_timings"` 28 | VerticalSyncTimings SyncTimings `json:"vertical_sync_timings"` 29 | Clock uint32 `json:"clock"` 30 | Width uint32 `json:"width"` 31 | Height uint32 `json:"height"` 32 | WidthMillimetres uint32 `json:"width_millimetres"` 33 | HeightMillimetres uint32 `json:"height_millimetres"` 34 | HorizontalFlag byte `json:"horizontal_flag"` 35 | VerticalFlag byte `json:"vertical_flag"` 36 | Vendor string `json:"vendor"` 37 | Name string `json:"name"` 38 | 39 | Serial string `json:"-"` 40 | } 41 | 42 | func (d DetailMonitor) DetailType() DetailType { 43 | return DetailTypeMonitor 44 | } 45 | 46 | func NewDetailMonitor(mon C.hd_detail_monitor_t) (*DetailMonitor, error) { 47 | data := mon.data 48 | 49 | return &DetailMonitor{ 50 | Type: DetailTypeMonitor, 51 | ManufactureYear: uint16(data.manu_year), 52 | ManufactureWeek: uint8(data.manu_week), 53 | VerticalSync: SyncRange{ 54 | Min: uint16(data.min_vsync), 55 | Max: uint16(data.max_vsync), 56 | }, 57 | HorizontalSync: SyncRange{ 58 | Min: uint16(data.min_hsync), 59 | Max: uint16(data.max_hsync), 60 | }, 61 | Clock: uint32(data.clock), 62 | Width: uint32(data.width), 63 | Height: uint32(data.height), 64 | WidthMillimetres: uint32(data.width_mm), 65 | HeightMillimetres: uint32(data.height_mm), 66 | HorizontalSyncTimings: SyncTimings{ 67 | Disp: uint16(data.hdisp), 68 | SyncStart: uint16(data.hsyncstart), 69 | SyncEnd: uint16(data.hsyncend), 70 | Total: uint32(data.htotal), 71 | }, 72 | VerticalSyncTimings: SyncTimings{ 73 | Disp: uint16(data.vdisp), 74 | SyncStart: uint16(data.vsyncstart), 75 | SyncEnd: uint16(data.vsyncend), 76 | Total: uint32(data.vtotal), 77 | }, 78 | HorizontalFlag: byte(data.hflag), 79 | VerticalFlag: byte(data.vflag), 80 | Vendor: C.GoString(data.vendor), 81 | Name: C.GoString(data.name), 82 | Serial: C.GoString(data.serial), 83 | }, nil 84 | } 85 | -------------------------------------------------------------------------------- /pkg/hwinfo/detail_enum_pci_flag.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=PciFlag -json -transform=snake -trimprefix PciFlag -output=./detail_enum_pci_flag.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _PciFlagName = "okpmagp" 12 | 13 | var _PciFlagIndex = [...]uint8{0, 2, 4, 7} 14 | 15 | const _PciFlagLowerName = "okpmagp" 16 | 17 | func (i PciFlag) String() string { 18 | if i >= PciFlag(len(_PciFlagIndex)-1) { 19 | return fmt.Sprintf("PciFlag(%d)", i) 20 | } 21 | return _PciFlagName[_PciFlagIndex[i]:_PciFlagIndex[i+1]] 22 | } 23 | 24 | // An "invalid array index" compiler error signifies that the constant values have changed. 25 | // Re-run the stringer command to generate them again. 26 | func _PciFlagNoOp() { 27 | var x [1]struct{} 28 | _ = x[PciFlagOk-(0)] 29 | _ = x[PciFlagPm-(1)] 30 | _ = x[PciFlagAgp-(2)] 31 | } 32 | 33 | var _PciFlagValues = []PciFlag{PciFlagOk, PciFlagPm, PciFlagAgp} 34 | 35 | var _PciFlagNameToValueMap = map[string]PciFlag{ 36 | _PciFlagName[0:2]: PciFlagOk, 37 | _PciFlagLowerName[0:2]: PciFlagOk, 38 | _PciFlagName[2:4]: PciFlagPm, 39 | _PciFlagLowerName[2:4]: PciFlagPm, 40 | _PciFlagName[4:7]: PciFlagAgp, 41 | _PciFlagLowerName[4:7]: PciFlagAgp, 42 | } 43 | 44 | var _PciFlagNames = []string{ 45 | _PciFlagName[0:2], 46 | _PciFlagName[2:4], 47 | _PciFlagName[4:7], 48 | } 49 | 50 | // PciFlagString retrieves an enum value from the enum constants string name. 51 | // Throws an error if the param is not part of the enum. 52 | func PciFlagString(s string) (PciFlag, error) { 53 | if val, ok := _PciFlagNameToValueMap[s]; ok { 54 | return val, nil 55 | } 56 | 57 | if val, ok := _PciFlagNameToValueMap[strings.ToLower(s)]; ok { 58 | return val, nil 59 | } 60 | return 0, fmt.Errorf("%s does not belong to PciFlag values", s) 61 | } 62 | 63 | // PciFlagValues returns all values of the enum 64 | func PciFlagValues() []PciFlag { 65 | return _PciFlagValues 66 | } 67 | 68 | // PciFlagStrings returns a slice of all String values of the enum 69 | func PciFlagStrings() []string { 70 | strs := make([]string, len(_PciFlagNames)) 71 | copy(strs, _PciFlagNames) 72 | return strs 73 | } 74 | 75 | // IsAPciFlag returns "true" if the value is listed in the enum definition. "false" otherwise 76 | func (i PciFlag) IsAPciFlag() bool { 77 | for _, v := range _PciFlagValues { 78 | if i == v { 79 | return true 80 | } 81 | } 82 | return false 83 | } 84 | 85 | // MarshalJSON implements the json.Marshaler interface for PciFlag 86 | func (i PciFlag) MarshalJSON() ([]byte, error) { 87 | return json.Marshal(i.String()) 88 | } 89 | 90 | // UnmarshalJSON implements the json.Unmarshaler interface for PciFlag 91 | func (i *PciFlag) UnmarshalJSON(data []byte) error { 92 | var s string 93 | if err := json.Unmarshal(data, &s); err != nil { 94 | return fmt.Errorf("PciFlag should be a string, got %s", data) 95 | } 96 | 97 | var err error 98 | *i, err = PciFlagString(s) 99 | return err 100 | } 101 | -------------------------------------------------------------------------------- /pkg/hwinfo/driver_info_x11.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | #include 7 | 8 | // custom getters to get around the problem with bitfields https://github.com/golang/go/issues/43261 9 | bool driver_info_x11_supports_3d(driver_info_x11_t info) { return info.x3d; } 10 | char driver_info_x11_colors_all(driver_info_x11_t info) { return info.colors.all; } 11 | char driver_info_x11_colors_c8(driver_info_x11_t info) { return info.colors.c8; } 12 | char driver_info_x11_colors_c15(driver_info_x11_t info) { return info.colors.c15; } 13 | char driver_info_x11_colors_c16(driver_info_x11_t info) { return info.colors.c16; } 14 | char driver_info_x11_colors_c24(driver_info_x11_t info) { return info.colors.c24; } 15 | char driver_info_x11_colors_c32(driver_info_x11_t info) { return info.colors.c32; } 16 | */ 17 | import "C" 18 | 19 | type DriverInfoX11 struct { 20 | Type DriverInfoType `json:"type,omitempty"` 21 | // actual driver database entries 22 | DBEntry0 []string `json:"db_entry_0,omitempty"` 23 | DBEntry1 []string `json:"db_entry_1,omitempty"` 24 | 25 | Server string `json:"server,omitempty"` // the server/module name 26 | XF86Version string `json:"xf86_version,omitempty"` // XFree86 version (3 or 4) 27 | Supports3D bool `json:"supports_3d"` // has 3D support 28 | Colors struct { 29 | // the next 5 entries combined 30 | All byte `json:"all"` 31 | C8 byte `json:"c8"` 32 | C15 byte `json:"c15"` 33 | C16 byte `json:"c16"` 34 | C24 byte `json:"c24"` 35 | C32 byte `json:"c32"` 36 | } `json:",omitempty"` 37 | DacSpeed uint32 `json:"dac_speed"` // max. ramdac clock 38 | Extensions []string `json:"extensions,omitempty"` // additional X extensions to load ('Module' section) 39 | Options []string `json:"options,omitempty"` // special server options 40 | Raw []string `json:"raw,omitempty"` // extra info to add to XF86Config 41 | Script string `json:"script"` // 3d script to run 42 | } 43 | 44 | func (d DriverInfoX11) DriverInfoType() DriverInfoType { 45 | return DriverInfoTypeX11 46 | } 47 | 48 | func NewDriverInfoX11(info C.driver_info_x11_t) DriverInfoX11 { 49 | result := DriverInfoX11{ 50 | Type: DriverInfoTypeX11, 51 | DBEntry0: ReadStringList(info.hddb0), 52 | DBEntry1: ReadStringList(info.hddb1), 53 | Server: C.GoString(info.server), 54 | XF86Version: C.GoString(info.xf86_ver), 55 | Supports3D: bool(C.driver_info_x11_supports_3d(info)), 56 | DacSpeed: uint32(info.dacspeed), 57 | Extensions: ReadStringList(info.extensions), 58 | Options: ReadStringList(info.options), 59 | Raw: ReadStringList(info.raw), 60 | Script: C.GoString(info.script), 61 | } 62 | 63 | result.Colors.All = byte(C.driver_info_x11_colors_all(info)) 64 | result.Colors.C8 = byte(C.driver_info_x11_colors_c8(info)) 65 | result.Colors.C15 = byte(C.driver_info_x11_colors_c15(info)) 66 | result.Colors.C16 = byte(C.driver_info_x11_colors_c16(info)) 67 | result.Colors.C24 = byte(C.driver_info_x11_colors_c24(info)) 68 | result.Colors.C32 = byte(C.driver_info_x11_colors_c32(info)) 69 | 70 | return result 71 | } 72 | -------------------------------------------------------------------------------- /pkg/hwinfo/detail.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | hd_detail_type_t hd_detail_get_type(hd_detail_t *det) { return det->type; } 9 | hd_detail_pci_t hd_detail_get_pci(hd_detail_t *det) { return det->pci; } 10 | hd_detail_usb_t hd_detail_get_usb(hd_detail_t *det) { return det->usb; } 11 | hd_detail_isapnp_t hd_detail_get_isapnp(hd_detail_t *det) { return det->isapnp; } 12 | hd_detail_cpu_t hd_detail_get_cpu(hd_detail_t *det) { return det->cpu; } 13 | hd_detail_monitor_t hd_detail_get_monitor(hd_detail_t *det) { return det->monitor; } 14 | hd_detail_bios_t hd_detail_get_bios(hd_detail_t *det) { return det->bios; } 15 | hd_detail_sys_t hd_detail_get_sys(hd_detail_t *det) { return det->sys; } 16 | 17 | */ 18 | import "C" 19 | 20 | import ( 21 | "encoding/hex" 22 | "errors" 23 | "fmt" 24 | "unsafe" 25 | ) 26 | 27 | //go:generate enumer -type=DetailType -json -transform=snake -trimprefix DetailType -output=./detail_enum_type.go 28 | type DetailType uint //nolint:recvcheck 29 | 30 | const ( 31 | DetailTypePci DetailType = iota 32 | DetailTypeUsb 33 | DetailTypeIsaPnp 34 | DetailTypeCdrom 35 | 36 | DetailTypeFloppy 37 | DetailTypeBios 38 | DetailTypeCpu 39 | DetailTypeProm 40 | 41 | DetailTypeMonitor 42 | DetailTypeSys 43 | DetailTypeScsi 44 | DetailTypeDevtree 45 | 46 | DetailTypeCcw 47 | DetailTypeJoystick 48 | ) 49 | 50 | type Detail interface { 51 | DetailType() DetailType 52 | } 53 | 54 | //nolint:ireturn 55 | func NewDetail(detail *C.hd_detail_t) (Detail, error) { 56 | if detail == nil { 57 | return nil, errors.New("detail is nil") 58 | } 59 | 60 | var ( 61 | err error 62 | result Detail 63 | ) 64 | 65 | switch DetailType(C.hd_detail_get_type(detail)) { 66 | case DetailTypePci: 67 | result, err = NewDetailPci(C.hd_detail_get_pci(detail)) 68 | case DetailTypeUsb: 69 | result, err = NewDetailUsb(C.hd_detail_get_usb(detail)) 70 | case DetailTypeIsaPnp: 71 | result, err = NewDetailIsaPnpDevice(C.hd_detail_get_isapnp(detail)) 72 | case DetailTypeCpu: 73 | result, err = NewDetailCPU(C.hd_detail_get_cpu(detail)) 74 | case DetailTypeMonitor: 75 | result, err = NewDetailMonitor(C.hd_detail_get_monitor(detail)) 76 | case DetailTypeBios: 77 | result, err = NewDetailBios(C.hd_detail_get_bios(detail)) 78 | case DetailTypeSys: 79 | result, err = NewDetailSys(C.hd_detail_get_sys(detail)) 80 | case DetailTypeCdrom, DetailTypeFloppy, DetailTypeProm, DetailTypeScsi, DetailTypeDevtree, DetailTypeCcw, 81 | DetailTypeJoystick: 82 | // do nothing for now 83 | 84 | default: 85 | err = fmt.Errorf("unknown detail type %d", DetailType(C.hd_detail_get_type(detail))) 86 | } 87 | 88 | return result, err 89 | } 90 | 91 | type MemoryRange struct { 92 | Start string `json:"start"` 93 | Size string `json:"size"` 94 | Data string `json:"-"` // hex encoded 95 | } 96 | 97 | func NewMemoryRange(mem C.memory_range_t) MemoryRange { 98 | return MemoryRange{ 99 | Start: fmt.Sprintf("0x%x", uint(mem.start)), 100 | Size: fmt.Sprintf("0x%x", uint(mem.size)), 101 | Data: hex.EncodeToString(C.GoBytes(unsafe.Pointer(&mem.data), C.int(mem.size))), //nolint:gocritic 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_enum_yes_no_flags.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=YesNoFlags -json -transform=snake -trimprefix YesNoFlags -output=./resource_enum_yes_no_flags.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _YesNoFlagsName = "unknownnoyes" 12 | 13 | var _YesNoFlagsIndex = [...]uint8{0, 7, 9, 12} 14 | 15 | const _YesNoFlagsLowerName = "unknownnoyes" 16 | 17 | func (i YesNoFlags) String() string { 18 | if i >= YesNoFlags(len(_YesNoFlagsIndex)-1) { 19 | return fmt.Sprintf("YesNoFlags(%d)", i) 20 | } 21 | return _YesNoFlagsName[_YesNoFlagsIndex[i]:_YesNoFlagsIndex[i+1]] 22 | } 23 | 24 | // An "invalid array index" compiler error signifies that the constant values have changed. 25 | // Re-run the stringer command to generate them again. 26 | func _YesNoFlagsNoOp() { 27 | var x [1]struct{} 28 | _ = x[YesNoFlagsUnknown-(0)] 29 | _ = x[YesNoFlagsNo-(1)] 30 | _ = x[YesNoFlagsYes-(2)] 31 | } 32 | 33 | var _YesNoFlagsValues = []YesNoFlags{YesNoFlagsUnknown, YesNoFlagsNo, YesNoFlagsYes} 34 | 35 | var _YesNoFlagsNameToValueMap = map[string]YesNoFlags{ 36 | _YesNoFlagsName[0:7]: YesNoFlagsUnknown, 37 | _YesNoFlagsLowerName[0:7]: YesNoFlagsUnknown, 38 | _YesNoFlagsName[7:9]: YesNoFlagsNo, 39 | _YesNoFlagsLowerName[7:9]: YesNoFlagsNo, 40 | _YesNoFlagsName[9:12]: YesNoFlagsYes, 41 | _YesNoFlagsLowerName[9:12]: YesNoFlagsYes, 42 | } 43 | 44 | var _YesNoFlagsNames = []string{ 45 | _YesNoFlagsName[0:7], 46 | _YesNoFlagsName[7:9], 47 | _YesNoFlagsName[9:12], 48 | } 49 | 50 | // YesNoFlagsString retrieves an enum value from the enum constants string name. 51 | // Throws an error if the param is not part of the enum. 52 | func YesNoFlagsString(s string) (YesNoFlags, error) { 53 | if val, ok := _YesNoFlagsNameToValueMap[s]; ok { 54 | return val, nil 55 | } 56 | 57 | if val, ok := _YesNoFlagsNameToValueMap[strings.ToLower(s)]; ok { 58 | return val, nil 59 | } 60 | return 0, fmt.Errorf("%s does not belong to YesNoFlags values", s) 61 | } 62 | 63 | // YesNoFlagsValues returns all values of the enum 64 | func YesNoFlagsValues() []YesNoFlags { 65 | return _YesNoFlagsValues 66 | } 67 | 68 | // YesNoFlagsStrings returns a slice of all String values of the enum 69 | func YesNoFlagsStrings() []string { 70 | strs := make([]string, len(_YesNoFlagsNames)) 71 | copy(strs, _YesNoFlagsNames) 72 | return strs 73 | } 74 | 75 | // IsAYesNoFlags returns "true" if the value is listed in the enum definition. "false" otherwise 76 | func (i YesNoFlags) IsAYesNoFlags() bool { 77 | for _, v := range _YesNoFlagsValues { 78 | if i == v { 79 | return true 80 | } 81 | } 82 | return false 83 | } 84 | 85 | // MarshalJSON implements the json.Marshaler interface for YesNoFlags 86 | func (i YesNoFlags) MarshalJSON() ([]byte, error) { 87 | return json.Marshal(i.String()) 88 | } 89 | 90 | // UnmarshalJSON implements the json.Unmarshaler interface for YesNoFlags 91 | func (i *YesNoFlags) UnmarshalJSON(data []byte) error { 92 | var s string 93 | if err := json.Unmarshal(data, &s); err != nil { 94 | return fmt.Errorf("YesNoFlags should be a string, got %s", data) 95 | } 96 | 97 | var err error 98 | *i, err = YesNoFlagsString(s) 99 | return err 100 | } 101 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project info 2 | site_name: NixOS Facter 3 | site_url: https://numtide.github.io/nixos-facter 4 | site_description: >- 5 | Declarative hardware configuration for NixOS 6 | 7 | # Repository 8 | repo_name: numtide/nixos-facter 9 | repo_url: https://github.com/nix-community/nixos-facter 10 | 11 | # Copyright 12 | copyright: >- 13 | Content on this site is licensed under a Creative Commons 14 | Attribution-ShareAlike 4.0 International license. 15 | 16 | validation: 17 | omitted_files: warn 18 | absolute_links: warn 19 | unrecognized_links: warn 20 | 21 | # Configuration 22 | 23 | docs_dir: docs/content 24 | 25 | nav: 26 | 27 | theme: 28 | name: material 29 | custom_dir: docs/theme 30 | 31 | logo: assets/images/logo.png 32 | favicon: assets/images/logo.png 33 | 34 | features: 35 | - content.code.annotate 36 | - content.code.copy 37 | - navigation.footer 38 | - navigation.indexes 39 | - navigation.path 40 | - navigation.sections 41 | - navigation.tabs 42 | - navigation.tracking 43 | - search.highlight 44 | - search.share 45 | - search.suggest 46 | 47 | font: 48 | text: Inter 49 | mono: Noto Sans Mono 50 | palette: 51 | # Palette toggle for automatic mode 52 | - media: "(prefers-color-scheme)" 53 | toggle: 54 | icon: material/brightness-auto 55 | name: Switch to light mode 56 | 57 | # Palette toggle for light mode 58 | - media: "(prefers-color-scheme: light)" 59 | scheme: default 60 | toggle: 61 | icon: material/brightness-7 62 | name: Switch to dark mode 63 | 64 | # Palette toggle for dark mode 65 | - media: "(prefers-color-scheme: dark)" 66 | scheme: slate 67 | toggle: 68 | icon: material/brightness-4 69 | name: Switch to system preference 70 | 71 | extra: 72 | version: 73 | provider: mike 74 | social: 75 | - icon: fontawesome/brands/github 76 | link: https://github.com/numtide 77 | - icon: fontawesome/brands/x-twitter 78 | link: https://x.com/numtide 79 | - icon: fontawesome/brands/mastodon 80 | link: https://fosstodon.org/@numtide 81 | 82 | extra_css: 83 | - stylesheets/extra.css 84 | 85 | markdown_extensions: 86 | - tables 87 | - admonition 88 | - attr_list 89 | - footnotes 90 | - md_in_html 91 | - def_list 92 | - meta 93 | - pymdownx.emoji: 94 | emoji_index: !!python/name:material.extensions.emoji.twemoji 95 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 96 | - pymdownx.tasklist: 97 | custom_checkbox: true 98 | - pymdownx.superfences 99 | - pymdownx.tabbed: 100 | alternate_style: true 101 | - pymdownx.details 102 | - pymdownx.highlight: 103 | use_pygments: true 104 | linenums: true 105 | anchor_linenums: true 106 | - pymdownx.inlinehilite 107 | - pymdownx.snippets 108 | - pymdownx.keys 109 | 110 | plugins: 111 | - search 112 | - mike 113 | -------------------------------------------------------------------------------- /nix/packages/nixos-facter/tests/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | inputs, 4 | perSystem, 5 | ... 6 | }: 7 | let 8 | # we have to import diskoLib like this because there are some impure default imports e.g. 9 | diskoLib = import "${inputs.disko}/lib" { 10 | inherit (pkgs) lib; 11 | makeTest = import "${inputs.nixpkgs}/nixos/tests/make-test-python.nix"; 12 | eval-config = import "${inputs.nixpkgs}/nixos/lib/eval-config.nix"; 13 | }; 14 | in 15 | # for now we only run the tests in x86_64-linux since we don't have access to a bare-metal ARM box or a VM that supports nested 16 | # virtualisation which makes the test take forever and ultimately fail 17 | pkgs.lib.optionalAttrs pkgs.stdenv.isx86_64 { 18 | golangci-lint = perSystem.self.nixos-facter.overrideAttrs (old: { 19 | nativeBuildInputs = old.nativeBuildInputs ++ [ pkgs.golangci-lint ]; 20 | buildPhase = '' 21 | HOME=$TMPDIR 22 | golangci-lint run 23 | ''; 24 | installPhase = '' 25 | touch $out 26 | ''; 27 | doInstallCheck = false; 28 | doCheck = false; 29 | }); 30 | 31 | basic = diskoLib.testLib.makeDiskoTest { 32 | inherit pkgs; 33 | name = "basic"; 34 | disko-config = ./disko.nix; 35 | extraSystemConfig = { 36 | environment.systemPackages = [ 37 | perSystem.self.nixos-facter 38 | ]; 39 | }; 40 | 41 | extraTestScript = '' 42 | import json 43 | 44 | report = json.loads(machine.succeed("nixos-facter --ephemeral 2>&1")) 45 | 46 | with subtest("Capture system"): 47 | assert report['system'] == '${pkgs.stdenv.hostPlatform.system}' 48 | 49 | with subtest("Capture virtualisation"): 50 | virt = report['virtualisation'] 51 | # kvm for systems that support it, otherwise the vm test should present itself as qemu 52 | # todo double-check this is the same for intel 53 | assert virt in ("kvm", "qemu"), f"expected virtualisation to be either kvm or qemu, got {virt}" 54 | 55 | with subtest("Capture UEFI boot information"): 56 | assert 'uefi' in report, "'uefi' not found in the report" 57 | 58 | uefi = report['uefi'] 59 | 60 | assert uefi['supported'] == True, f"expected UEFI to be supported, got {uefi['supported']}" 61 | assert 'platform_size' in uefi, "'platform_size' not found in uefi" 62 | assert uefi['platform_size'] in (32, 64), f"expected platform_size to be 32 or 64, got {uefi['platform_size']}" 63 | 64 | with subtest("Capture swap entries"): 65 | assert 'swap' in report, "'swap' not found in the report" 66 | 67 | swap = report['swap'] 68 | 69 | expected = [ 70 | { 'type': 'partition', 'size': 1048572, 'used': 0, 'priority': -2 }, 71 | { 'type': 'partition', 'size': 10236, 'used': 0, 'priority': 100 } 72 | ] 73 | 74 | assert len(swap) == len(expected), f"expected {len(expected)} swap entries, found {len(swap)}" 75 | 76 | for i in range(2): 77 | assert swap[i]['path'].startswith("/dev/disk/by-uuid/"), f"expected a stable device path: {swap[i]['path']}" 78 | 79 | # delete for easier comparison 80 | del swap[i]['path'] 81 | 82 | assert swap[i] == expected[i], f"swap[{i}] != expected[{i}] mismatch: {swap[i]} != {expected[i]}" 83 | ''; 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /pkg/hwinfo/hardware_enum_sub_class_keyboard.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=SubClassKeyboard -json -transform=snake -trimprefix SubClassKeyboard -output=./hardware_enum_sub_class_keyboard.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _SubClassKeyboardName = "kbdconsole" 12 | 13 | var _SubClassKeyboardIndex = [...]uint8{0, 3, 10} 14 | 15 | const _SubClassKeyboardLowerName = "kbdconsole" 16 | 17 | func (i SubClassKeyboard) String() string { 18 | if i >= SubClassKeyboard(len(_SubClassKeyboardIndex)-1) { 19 | return fmt.Sprintf("SubClassKeyboard(%d)", i) 20 | } 21 | return _SubClassKeyboardName[_SubClassKeyboardIndex[i]:_SubClassKeyboardIndex[i+1]] 22 | } 23 | 24 | // An "invalid array index" compiler error signifies that the constant values have changed. 25 | // Re-run the stringer command to generate them again. 26 | func _SubClassKeyboardNoOp() { 27 | var x [1]struct{} 28 | _ = x[SubClassKeyboardKbd-(0)] 29 | _ = x[SubClassKeyboardConsole-(1)] 30 | } 31 | 32 | var _SubClassKeyboardValues = []SubClassKeyboard{SubClassKeyboardKbd, SubClassKeyboardConsole} 33 | 34 | var _SubClassKeyboardNameToValueMap = map[string]SubClassKeyboard{ 35 | _SubClassKeyboardName[0:3]: SubClassKeyboardKbd, 36 | _SubClassKeyboardLowerName[0:3]: SubClassKeyboardKbd, 37 | _SubClassKeyboardName[3:10]: SubClassKeyboardConsole, 38 | _SubClassKeyboardLowerName[3:10]: SubClassKeyboardConsole, 39 | } 40 | 41 | var _SubClassKeyboardNames = []string{ 42 | _SubClassKeyboardName[0:3], 43 | _SubClassKeyboardName[3:10], 44 | } 45 | 46 | // SubClassKeyboardString retrieves an enum value from the enum constants string name. 47 | // Throws an error if the param is not part of the enum. 48 | func SubClassKeyboardString(s string) (SubClassKeyboard, error) { 49 | if val, ok := _SubClassKeyboardNameToValueMap[s]; ok { 50 | return val, nil 51 | } 52 | 53 | if val, ok := _SubClassKeyboardNameToValueMap[strings.ToLower(s)]; ok { 54 | return val, nil 55 | } 56 | return 0, fmt.Errorf("%s does not belong to SubClassKeyboard values", s) 57 | } 58 | 59 | // SubClassKeyboardValues returns all values of the enum 60 | func SubClassKeyboardValues() []SubClassKeyboard { 61 | return _SubClassKeyboardValues 62 | } 63 | 64 | // SubClassKeyboardStrings returns a slice of all String values of the enum 65 | func SubClassKeyboardStrings() []string { 66 | strs := make([]string, len(_SubClassKeyboardNames)) 67 | copy(strs, _SubClassKeyboardNames) 68 | return strs 69 | } 70 | 71 | // IsASubClassKeyboard returns "true" if the value is listed in the enum definition. "false" otherwise 72 | func (i SubClassKeyboard) IsASubClassKeyboard() bool { 73 | for _, v := range _SubClassKeyboardValues { 74 | if i == v { 75 | return true 76 | } 77 | } 78 | return false 79 | } 80 | 81 | // MarshalJSON implements the json.Marshaler interface for SubClassKeyboard 82 | func (i SubClassKeyboard) MarshalJSON() ([]byte, error) { 83 | return json.Marshal(i.String()) 84 | } 85 | 86 | // UnmarshalJSON implements the json.Unmarshaler interface for SubClassKeyboard 87 | func (i *SubClassKeyboard) UnmarshalJSON(data []byte) error { 88 | var s string 89 | if err := json.Unmarshal(data, &s); err != nil { 90 | return fmt.Errorf("SubClassKeyboard should be a string, got %s", data) 91 | } 92 | 93 | var err error 94 | *i, err = SubClassKeyboardString(s) 95 | return err 96 | } 97 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_enum_geo_type.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=GeoType -json -transform=snake -trimprefix GeoType -output=./resource_enum_geo_type.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _GeoTypeName = "physicallogicalbios_eddbios_legacy" 12 | 13 | var _GeoTypeIndex = [...]uint8{0, 8, 15, 23, 34} 14 | 15 | const _GeoTypeLowerName = "physicallogicalbios_eddbios_legacy" 16 | 17 | func (i GeoType) String() string { 18 | if i >= GeoType(len(_GeoTypeIndex)-1) { 19 | return fmt.Sprintf("GeoType(%d)", i) 20 | } 21 | return _GeoTypeName[_GeoTypeIndex[i]:_GeoTypeIndex[i+1]] 22 | } 23 | 24 | // An "invalid array index" compiler error signifies that the constant values have changed. 25 | // Re-run the stringer command to generate them again. 26 | func _GeoTypeNoOp() { 27 | var x [1]struct{} 28 | _ = x[GeoTypePhysical-(0)] 29 | _ = x[GeoTypeLogical-(1)] 30 | _ = x[GeoTypeBiosEdd-(2)] 31 | _ = x[GeoTypeBiosLegacy-(3)] 32 | } 33 | 34 | var _GeoTypeValues = []GeoType{GeoTypePhysical, GeoTypeLogical, GeoTypeBiosEdd, GeoTypeBiosLegacy} 35 | 36 | var _GeoTypeNameToValueMap = map[string]GeoType{ 37 | _GeoTypeName[0:8]: GeoTypePhysical, 38 | _GeoTypeLowerName[0:8]: GeoTypePhysical, 39 | _GeoTypeName[8:15]: GeoTypeLogical, 40 | _GeoTypeLowerName[8:15]: GeoTypeLogical, 41 | _GeoTypeName[15:23]: GeoTypeBiosEdd, 42 | _GeoTypeLowerName[15:23]: GeoTypeBiosEdd, 43 | _GeoTypeName[23:34]: GeoTypeBiosLegacy, 44 | _GeoTypeLowerName[23:34]: GeoTypeBiosLegacy, 45 | } 46 | 47 | var _GeoTypeNames = []string{ 48 | _GeoTypeName[0:8], 49 | _GeoTypeName[8:15], 50 | _GeoTypeName[15:23], 51 | _GeoTypeName[23:34], 52 | } 53 | 54 | // GeoTypeString retrieves an enum value from the enum constants string name. 55 | // Throws an error if the param is not part of the enum. 56 | func GeoTypeString(s string) (GeoType, error) { 57 | if val, ok := _GeoTypeNameToValueMap[s]; ok { 58 | return val, nil 59 | } 60 | 61 | if val, ok := _GeoTypeNameToValueMap[strings.ToLower(s)]; ok { 62 | return val, nil 63 | } 64 | return 0, fmt.Errorf("%s does not belong to GeoType values", s) 65 | } 66 | 67 | // GeoTypeValues returns all values of the enum 68 | func GeoTypeValues() []GeoType { 69 | return _GeoTypeValues 70 | } 71 | 72 | // GeoTypeStrings returns a slice of all String values of the enum 73 | func GeoTypeStrings() []string { 74 | strs := make([]string, len(_GeoTypeNames)) 75 | copy(strs, _GeoTypeNames) 76 | return strs 77 | } 78 | 79 | // IsAGeoType returns "true" if the value is listed in the enum definition. "false" otherwise 80 | func (i GeoType) IsAGeoType() bool { 81 | for _, v := range _GeoTypeValues { 82 | if i == v { 83 | return true 84 | } 85 | } 86 | return false 87 | } 88 | 89 | // MarshalJSON implements the json.Marshaler interface for GeoType 90 | func (i GeoType) MarshalJSON() ([]byte, error) { 91 | return json.Marshal(i.String()) 92 | } 93 | 94 | // UnmarshalJSON implements the json.Unmarshaler interface for GeoType 95 | func (i *GeoType) UnmarshalJSON(data []byte) error { 96 | var s string 97 | if err := json.Unmarshal(data, &s); err != nil { 98 | return fmt.Errorf("GeoType should be a string, got %s", data) 99 | } 100 | 101 | var err error 102 | *i, err = GeoTypeString(s) 103 | return err 104 | } 105 | -------------------------------------------------------------------------------- /pkg/hwinfo/id_tag_enum.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=IDTag -json -transform=snake -trimprefix IDTag -output=./id_tag_enum.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _IDTagName = "pcieisausbspecialpcmciasdio" 12 | 13 | var _IDTagIndex = [...]uint8{0, 3, 7, 10, 17, 23, 27} 14 | 15 | const _IDTagLowerName = "pcieisausbspecialpcmciasdio" 16 | 17 | func (i IDTag) String() string { 18 | i -= 1 19 | if i >= IDTag(len(_IDTagIndex)-1) { 20 | return fmt.Sprintf("IDTag(%d)", i+1) 21 | } 22 | return _IDTagName[_IDTagIndex[i]:_IDTagIndex[i+1]] 23 | } 24 | 25 | // An "invalid array index" compiler error signifies that the constant values have changed. 26 | // Re-run the stringer command to generate them again. 27 | func _IDTagNoOp() { 28 | var x [1]struct{} 29 | _ = x[IDTagPci-(1)] 30 | _ = x[IDTagEisa-(2)] 31 | _ = x[IDTagUsb-(3)] 32 | _ = x[IDTagSpecial-(4)] 33 | _ = x[IDTagPcmcia-(5)] 34 | _ = x[IDTagSdio-(6)] 35 | } 36 | 37 | var _IDTagValues = []IDTag{IDTagPci, IDTagEisa, IDTagUsb, IDTagSpecial, IDTagPcmcia, IDTagSdio} 38 | 39 | var _IDTagNameToValueMap = map[string]IDTag{ 40 | _IDTagName[0:3]: IDTagPci, 41 | _IDTagLowerName[0:3]: IDTagPci, 42 | _IDTagName[3:7]: IDTagEisa, 43 | _IDTagLowerName[3:7]: IDTagEisa, 44 | _IDTagName[7:10]: IDTagUsb, 45 | _IDTagLowerName[7:10]: IDTagUsb, 46 | _IDTagName[10:17]: IDTagSpecial, 47 | _IDTagLowerName[10:17]: IDTagSpecial, 48 | _IDTagName[17:23]: IDTagPcmcia, 49 | _IDTagLowerName[17:23]: IDTagPcmcia, 50 | _IDTagName[23:27]: IDTagSdio, 51 | _IDTagLowerName[23:27]: IDTagSdio, 52 | } 53 | 54 | var _IDTagNames = []string{ 55 | _IDTagName[0:3], 56 | _IDTagName[3:7], 57 | _IDTagName[7:10], 58 | _IDTagName[10:17], 59 | _IDTagName[17:23], 60 | _IDTagName[23:27], 61 | } 62 | 63 | // IDTagString retrieves an enum value from the enum constants string name. 64 | // Throws an error if the param is not part of the enum. 65 | func IDTagString(s string) (IDTag, error) { 66 | if val, ok := _IDTagNameToValueMap[s]; ok { 67 | return val, nil 68 | } 69 | 70 | if val, ok := _IDTagNameToValueMap[strings.ToLower(s)]; ok { 71 | return val, nil 72 | } 73 | return 0, fmt.Errorf("%s does not belong to IDTag values", s) 74 | } 75 | 76 | // IDTagValues returns all values of the enum 77 | func IDTagValues() []IDTag { 78 | return _IDTagValues 79 | } 80 | 81 | // IDTagStrings returns a slice of all String values of the enum 82 | func IDTagStrings() []string { 83 | strs := make([]string, len(_IDTagNames)) 84 | copy(strs, _IDTagNames) 85 | return strs 86 | } 87 | 88 | // IsAIDTag returns "true" if the value is listed in the enum definition. "false" otherwise 89 | func (i IDTag) IsAIDTag() bool { 90 | for _, v := range _IDTagValues { 91 | if i == v { 92 | return true 93 | } 94 | } 95 | return false 96 | } 97 | 98 | // MarshalJSON implements the json.Marshaler interface for IDTag 99 | func (i IDTag) MarshalJSON() ([]byte, error) { 100 | return json.Marshal(i.String()) 101 | } 102 | 103 | // UnmarshalJSON implements the json.Unmarshaler interface for IDTag 104 | func (i *IDTag) UnmarshalJSON(data []byte) error { 105 | var s string 106 | if err := json.Unmarshal(data, &s); err != nil { 107 | return fmt.Errorf("IDTag should be a string, got %s", data) 108 | } 109 | 110 | var err error 111 | *i, err = IDTagString(s) 112 | return err 113 | } 114 | -------------------------------------------------------------------------------- /pkg/hwinfo/hardware_enum_hotplug.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=Hotplug -json -transform=snake -trimprefix Hotplug -output=./hardware_enum_hotplug.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _HotplugName = "nonepcmciacardbuspciusbfirewire" 12 | 13 | var _HotplugIndex = [...]uint8{0, 4, 10, 17, 20, 23, 31} 14 | 15 | const _HotplugLowerName = "nonepcmciacardbuspciusbfirewire" 16 | 17 | func (i Hotplug) String() string { 18 | if i < 0 || i >= Hotplug(len(_HotplugIndex)-1) { 19 | return fmt.Sprintf("Hotplug(%d)", i) 20 | } 21 | return _HotplugName[_HotplugIndex[i]:_HotplugIndex[i+1]] 22 | } 23 | 24 | // An "invalid array index" compiler error signifies that the constant values have changed. 25 | // Re-run the stringer command to generate them again. 26 | func _HotplugNoOp() { 27 | var x [1]struct{} 28 | _ = x[HotplugNone-(0)] 29 | _ = x[HotplugPcmcia-(1)] 30 | _ = x[HotplugCardbus-(2)] 31 | _ = x[HotplugPci-(3)] 32 | _ = x[HotplugUsb-(4)] 33 | _ = x[HotplugFirewire-(5)] 34 | } 35 | 36 | var _HotplugValues = []Hotplug{HotplugNone, HotplugPcmcia, HotplugCardbus, HotplugPci, HotplugUsb, HotplugFirewire} 37 | 38 | var _HotplugNameToValueMap = map[string]Hotplug{ 39 | _HotplugName[0:4]: HotplugNone, 40 | _HotplugLowerName[0:4]: HotplugNone, 41 | _HotplugName[4:10]: HotplugPcmcia, 42 | _HotplugLowerName[4:10]: HotplugPcmcia, 43 | _HotplugName[10:17]: HotplugCardbus, 44 | _HotplugLowerName[10:17]: HotplugCardbus, 45 | _HotplugName[17:20]: HotplugPci, 46 | _HotplugLowerName[17:20]: HotplugPci, 47 | _HotplugName[20:23]: HotplugUsb, 48 | _HotplugLowerName[20:23]: HotplugUsb, 49 | _HotplugName[23:31]: HotplugFirewire, 50 | _HotplugLowerName[23:31]: HotplugFirewire, 51 | } 52 | 53 | var _HotplugNames = []string{ 54 | _HotplugName[0:4], 55 | _HotplugName[4:10], 56 | _HotplugName[10:17], 57 | _HotplugName[17:20], 58 | _HotplugName[20:23], 59 | _HotplugName[23:31], 60 | } 61 | 62 | // HotplugString retrieves an enum value from the enum constants string name. 63 | // Throws an error if the param is not part of the enum. 64 | func HotplugString(s string) (Hotplug, error) { 65 | if val, ok := _HotplugNameToValueMap[s]; ok { 66 | return val, nil 67 | } 68 | 69 | if val, ok := _HotplugNameToValueMap[strings.ToLower(s)]; ok { 70 | return val, nil 71 | } 72 | return 0, fmt.Errorf("%s does not belong to Hotplug values", s) 73 | } 74 | 75 | // HotplugValues returns all values of the enum 76 | func HotplugValues() []Hotplug { 77 | return _HotplugValues 78 | } 79 | 80 | // HotplugStrings returns a slice of all String values of the enum 81 | func HotplugStrings() []string { 82 | strs := make([]string, len(_HotplugNames)) 83 | copy(strs, _HotplugNames) 84 | return strs 85 | } 86 | 87 | // IsAHotplug returns "true" if the value is listed in the enum definition. "false" otherwise 88 | func (i Hotplug) IsAHotplug() bool { 89 | for _, v := range _HotplugValues { 90 | if i == v { 91 | return true 92 | } 93 | } 94 | return false 95 | } 96 | 97 | // MarshalJSON implements the json.Marshaler interface for Hotplug 98 | func (i Hotplug) MarshalJSON() ([]byte, error) { 99 | return json.Marshal(i.String()) 100 | } 101 | 102 | // UnmarshalJSON implements the json.Unmarshaler interface for Hotplug 103 | func (i *Hotplug) UnmarshalJSON(data []byte) error { 104 | var s string 105 | if err := json.Unmarshal(data, &s); err != nil { 106 | return fmt.Errorf("Hotplug should be a string, got %s", data) 107 | } 108 | 109 | var err error 110 | *i, err = HotplugString(s) 111 | return err 112 | } 113 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_enum_access_flags.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=AccessFlags -json -transform=snake -trimprefix AccessFlags -output=./resource_enum_access_flags.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _AccessFlagsName = "unknownread_onlywrite_onlyread_write" 12 | 13 | var _AccessFlagsIndex = [...]uint8{0, 7, 16, 26, 36} 14 | 15 | const _AccessFlagsLowerName = "unknownread_onlywrite_onlyread_write" 16 | 17 | func (i AccessFlags) String() string { 18 | if i >= AccessFlags(len(_AccessFlagsIndex)-1) { 19 | return fmt.Sprintf("AccessFlags(%d)", i) 20 | } 21 | return _AccessFlagsName[_AccessFlagsIndex[i]:_AccessFlagsIndex[i+1]] 22 | } 23 | 24 | // An "invalid array index" compiler error signifies that the constant values have changed. 25 | // Re-run the stringer command to generate them again. 26 | func _AccessFlagsNoOp() { 27 | var x [1]struct{} 28 | _ = x[AccessFlagsUnknown-(0)] 29 | _ = x[AccessFlagsReadOnly-(1)] 30 | _ = x[AccessFlagsWriteOnly-(2)] 31 | _ = x[AccessFlagsReadWrite-(3)] 32 | } 33 | 34 | var _AccessFlagsValues = []AccessFlags{AccessFlagsUnknown, AccessFlagsReadOnly, AccessFlagsWriteOnly, AccessFlagsReadWrite} 35 | 36 | var _AccessFlagsNameToValueMap = map[string]AccessFlags{ 37 | _AccessFlagsName[0:7]: AccessFlagsUnknown, 38 | _AccessFlagsLowerName[0:7]: AccessFlagsUnknown, 39 | _AccessFlagsName[7:16]: AccessFlagsReadOnly, 40 | _AccessFlagsLowerName[7:16]: AccessFlagsReadOnly, 41 | _AccessFlagsName[16:26]: AccessFlagsWriteOnly, 42 | _AccessFlagsLowerName[16:26]: AccessFlagsWriteOnly, 43 | _AccessFlagsName[26:36]: AccessFlagsReadWrite, 44 | _AccessFlagsLowerName[26:36]: AccessFlagsReadWrite, 45 | } 46 | 47 | var _AccessFlagsNames = []string{ 48 | _AccessFlagsName[0:7], 49 | _AccessFlagsName[7:16], 50 | _AccessFlagsName[16:26], 51 | _AccessFlagsName[26:36], 52 | } 53 | 54 | // AccessFlagsString retrieves an enum value from the enum constants string name. 55 | // Throws an error if the param is not part of the enum. 56 | func AccessFlagsString(s string) (AccessFlags, error) { 57 | if val, ok := _AccessFlagsNameToValueMap[s]; ok { 58 | return val, nil 59 | } 60 | 61 | if val, ok := _AccessFlagsNameToValueMap[strings.ToLower(s)]; ok { 62 | return val, nil 63 | } 64 | return 0, fmt.Errorf("%s does not belong to AccessFlags values", s) 65 | } 66 | 67 | // AccessFlagsValues returns all values of the enum 68 | func AccessFlagsValues() []AccessFlags { 69 | return _AccessFlagsValues 70 | } 71 | 72 | // AccessFlagsStrings returns a slice of all String values of the enum 73 | func AccessFlagsStrings() []string { 74 | strs := make([]string, len(_AccessFlagsNames)) 75 | copy(strs, _AccessFlagsNames) 76 | return strs 77 | } 78 | 79 | // IsAAccessFlags returns "true" if the value is listed in the enum definition. "false" otherwise 80 | func (i AccessFlags) IsAAccessFlags() bool { 81 | for _, v := range _AccessFlagsValues { 82 | if i == v { 83 | return true 84 | } 85 | } 86 | return false 87 | } 88 | 89 | // MarshalJSON implements the json.Marshaler interface for AccessFlags 90 | func (i AccessFlags) MarshalJSON() ([]byte, error) { 91 | return json.Marshal(i.String()) 92 | } 93 | 94 | // UnmarshalJSON implements the json.Unmarshaler interface for AccessFlags 95 | func (i *AccessFlags) UnmarshalJSON(data []byte) error { 96 | var s string 97 | if err := json.Unmarshal(data, &s); err != nil { 98 | return fmt.Errorf("AccessFlags should be a string, got %s", data) 99 | } 100 | 101 | var err error 102 | *i, err = AccessFlagsString(s) 103 | return err 104 | } 105 | -------------------------------------------------------------------------------- /pkg/ephem/swap.go: -------------------------------------------------------------------------------- 1 | package ephem 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "regexp" 10 | "sort" 11 | "strconv" 12 | ) 13 | 14 | var ( 15 | swapHeaderRegex = regexp.MustCompile(`^Filename\s+Type\s+Size\s+Used\s+Priority$`) 16 | swapEntryRegex = regexp.MustCompile(`^(.*?)\s+(partition|file)\s+(.*?)\s+(.*?)\s+(.*?)$`) 17 | ) 18 | 19 | // SwapType represents the type of swap space, either file or partition. 20 | // 21 | //go:generate enumer -type=SwapType -json -transform=snake -trimprefix SwapType -output=./swap_enum_type.go 22 | type SwapType uint //nolint:recvcheck 23 | 24 | const ( 25 | SwapTypeFile SwapType = iota 26 | SwapTypePartition 27 | ) 28 | 29 | // SwapEntry represents a swap entry. 30 | type SwapEntry struct { 31 | // Type is the type of swap e.g. partition or file. 32 | Type SwapType `json:"type"` 33 | // Filename is the path to the swap device or file. 34 | Filename string `json:"path"` 35 | // Size is the total size of the swap in kilobytes. 36 | Size uint64 `json:"size"` 37 | // Used is the amount of swap space currently in use, in kilobytes. 38 | Used uint64 `json:"used"` 39 | // Priority determines the order in which swap spaces are used. 40 | // Higher numbers have higher priority. 41 | Priority int32 `json:"priority"` 42 | } 43 | 44 | // SwapEntries retrieves the list of swap entries from the system and resolves stable device paths for each entry. 45 | func SwapEntries() ([]*SwapEntry, error) { 46 | f, err := os.Open("/proc/swaps") 47 | if err != nil { 48 | return nil, fmt.Errorf("failed to open /proc/swaps: %w", err) 49 | } 50 | 51 | devices, err := ReadSwapFile(f) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | for idx := range devices { 57 | // try to resolve stable device paths for each swap device 58 | stablePath, err := StableDevicePath(devices[idx].Filename) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | devices[idx].Filename = stablePath 64 | } 65 | 66 | // Sort by priority (ascending), then by filename for deterministic output 67 | sort.Slice(devices, func(i, j int) bool { 68 | if devices[i].Priority != devices[j].Priority { 69 | return devices[i].Priority < devices[j].Priority 70 | } 71 | return devices[i].Filename < devices[j].Filename 72 | }) 73 | 74 | return devices, nil 75 | } 76 | 77 | // ReadSwapFile reads swap entries from an io.Reader, validating the format and parsing each entry. 78 | func ReadSwapFile(reader io.Reader) ([]*SwapEntry, error) { 79 | scanner := bufio.NewScanner(reader) 80 | if !scanner.Scan() { 81 | return nil, errors.New("swaps file is empty") 82 | } else if b := scanner.Bytes(); !swapHeaderRegex.Match(b) { 83 | return nil, fmt.Errorf("header in swaps file is malformed: '%s'", string(b)) 84 | } 85 | 86 | var result []*SwapEntry 87 | 88 | for scanner.Scan() { 89 | line := scanner.Text() 90 | 91 | matches := swapEntryRegex.FindAllStringSubmatch(line, 1) 92 | if len(matches) != 1 { 93 | return nil, fmt.Errorf("malformed entry in swaps file: '%s'", line) 94 | } 95 | 96 | fields := matches[0] 97 | 98 | swapType, err := SwapTypeString(fields[2]) 99 | if err != nil { 100 | return nil, fmt.Errorf("malformed swap type: '%s'", fields[2]) 101 | } 102 | 103 | size, err := strconv.ParseUint(fields[3], 10, 64) 104 | if err != nil { 105 | return nil, fmt.Errorf("malformed size value: '%s'", fields[3]) 106 | } 107 | 108 | used, err := strconv.ParseUint(fields[4], 10, 64) 109 | if err != nil { 110 | return nil, fmt.Errorf("malformed used value: '%s'", fields[4]) 111 | } 112 | 113 | priority, err := strconv.ParseInt(fields[5], 10, 32) 114 | if err != nil { 115 | return nil, fmt.Errorf("malformed priority value: '%s'", fields[5]) 116 | } 117 | 118 | result = append(result, &SwapEntry{ 119 | Filename: fields[1], 120 | Type: swapType, 121 | Size: size, 122 | Used: used, 123 | Priority: int32(priority), 124 | }) 125 | } 126 | 127 | return result, nil 128 | } 129 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource_enum_size_unit.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=SizeUnit -json --transform=snake -trimprefix SizeUnit --output=./resource_enum_size_unit.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _SizeUnitName = "cmcinchbytesectorskbytembytegbytemm" 12 | 13 | var _SizeUnitIndex = [...]uint8{0, 2, 7, 11, 18, 23, 28, 33, 35} 14 | 15 | const _SizeUnitLowerName = "cmcinchbytesectorskbytembytegbytemm" 16 | 17 | func (i SizeUnit) String() string { 18 | if i >= SizeUnit(len(_SizeUnitIndex)-1) { 19 | return fmt.Sprintf("SizeUnit(%d)", i) 20 | } 21 | return _SizeUnitName[_SizeUnitIndex[i]:_SizeUnitIndex[i+1]] 22 | } 23 | 24 | // An "invalid array index" compiler error signifies that the constant values have changed. 25 | // Re-run the stringer command to generate them again. 26 | func _SizeUnitNoOp() { 27 | var x [1]struct{} 28 | _ = x[SizeUnitCm-(0)] 29 | _ = x[SizeUnitCinch-(1)] 30 | _ = x[SizeUnitByte-(2)] 31 | _ = x[SizeUnitSectors-(3)] 32 | _ = x[SizeUnitKbyte-(4)] 33 | _ = x[SizeUnitMbyte-(5)] 34 | _ = x[SizeUnitGbyte-(6)] 35 | _ = x[SizeUnitMm-(7)] 36 | } 37 | 38 | var _SizeUnitValues = []SizeUnit{SizeUnitCm, SizeUnitCinch, SizeUnitByte, SizeUnitSectors, SizeUnitKbyte, SizeUnitMbyte, SizeUnitGbyte, SizeUnitMm} 39 | 40 | var _SizeUnitNameToValueMap = map[string]SizeUnit{ 41 | _SizeUnitName[0:2]: SizeUnitCm, 42 | _SizeUnitLowerName[0:2]: SizeUnitCm, 43 | _SizeUnitName[2:7]: SizeUnitCinch, 44 | _SizeUnitLowerName[2:7]: SizeUnitCinch, 45 | _SizeUnitName[7:11]: SizeUnitByte, 46 | _SizeUnitLowerName[7:11]: SizeUnitByte, 47 | _SizeUnitName[11:18]: SizeUnitSectors, 48 | _SizeUnitLowerName[11:18]: SizeUnitSectors, 49 | _SizeUnitName[18:23]: SizeUnitKbyte, 50 | _SizeUnitLowerName[18:23]: SizeUnitKbyte, 51 | _SizeUnitName[23:28]: SizeUnitMbyte, 52 | _SizeUnitLowerName[23:28]: SizeUnitMbyte, 53 | _SizeUnitName[28:33]: SizeUnitGbyte, 54 | _SizeUnitLowerName[28:33]: SizeUnitGbyte, 55 | _SizeUnitName[33:35]: SizeUnitMm, 56 | _SizeUnitLowerName[33:35]: SizeUnitMm, 57 | } 58 | 59 | var _SizeUnitNames = []string{ 60 | _SizeUnitName[0:2], 61 | _SizeUnitName[2:7], 62 | _SizeUnitName[7:11], 63 | _SizeUnitName[11:18], 64 | _SizeUnitName[18:23], 65 | _SizeUnitName[23:28], 66 | _SizeUnitName[28:33], 67 | _SizeUnitName[33:35], 68 | } 69 | 70 | // SizeUnitString retrieves an enum value from the enum constants string name. 71 | // Throws an error if the param is not part of the enum. 72 | func SizeUnitString(s string) (SizeUnit, error) { 73 | if val, ok := _SizeUnitNameToValueMap[s]; ok { 74 | return val, nil 75 | } 76 | 77 | if val, ok := _SizeUnitNameToValueMap[strings.ToLower(s)]; ok { 78 | return val, nil 79 | } 80 | return 0, fmt.Errorf("%s does not belong to SizeUnit values", s) 81 | } 82 | 83 | // SizeUnitValues returns all values of the enum 84 | func SizeUnitValues() []SizeUnit { 85 | return _SizeUnitValues 86 | } 87 | 88 | // SizeUnitStrings returns a slice of all String values of the enum 89 | func SizeUnitStrings() []string { 90 | strs := make([]string, len(_SizeUnitNames)) 91 | copy(strs, _SizeUnitNames) 92 | return strs 93 | } 94 | 95 | // IsASizeUnit returns "true" if the value is listed in the enum definition. "false" otherwise 96 | func (i SizeUnit) IsASizeUnit() bool { 97 | for _, v := range _SizeUnitValues { 98 | if i == v { 99 | return true 100 | } 101 | } 102 | return false 103 | } 104 | 105 | // MarshalJSON implements the json.Marshaler interface for SizeUnit 106 | func (i SizeUnit) MarshalJSON() ([]byte, error) { 107 | return json.Marshal(i.String()) 108 | } 109 | 110 | // UnmarshalJSON implements the json.Unmarshaler interface for SizeUnit 111 | func (i *SizeUnit) UnmarshalJSON(data []byte) error { 112 | var s string 113 | if err := json.Unmarshal(data, &s); err != nil { 114 | return fmt.Errorf("SizeUnit should be a string, got %s", data) 115 | } 116 | 117 | var err error 118 | *i, err = SizeUnitString(s) 119 | return err 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NixOS Facter 2 | 3 | NixOS Facter aims to be an alternative to projects such as [NixOS Hardware] and [nixos-generate-config]. 4 | It solves the problem of bootstrapping [NixOS configurations] by deferring decisions about hardware and other 5 | aspects of the target platform to NixOS modules. 6 | 7 | We do this by first generating a machine-readable report (JSON) which captures detailed information about the machine 8 | or virtual environment it was executed within. 9 | 10 | This report is then passed to a series of [NixOS modules] which can make a variety of decisions, 11 | some simple, some more complex, enabling things like automatic configuration of network controllers or graphics cards, 12 | USB devices, and so on. 13 | 14 | ## Project Structure 15 | 16 | This repository contains the binary for generating the report. 17 | 18 | [NixOS Facter Modules] contains the necessary NixOS modules for making use of the report in a NixOS configuration. 19 | 20 | For more information, please see the [docs]. 21 | 22 | ## Quick Start 23 | 24 | To generate a report using `nixos-facter` from nixpkgs: 25 | 26 | ```console 27 | # you must run this as root 28 | ❯ sudo nix run --option experimental-features "nix-command flakes" nixpkgs#nixos-facter -- -o facter.json 29 | ``` 30 | 31 | To use the latest development version from this flake: 32 | 33 | ```console 34 | # you must run this as root 35 | ❯ sudo nix run \ 36 | --option experimental-features "nix-command flakes" \ 37 | --option extra-substituters https://numtide.cachix.org \ 38 | --option extra-trusted-public-keys numtide.cachix.org-1:2ps1kLBUWjxIneOy1Ik6cQjb41X0iXVXeHigGmycPPE= \ 39 | github:nix-community/nixos-facter -- -o facter.json 40 | ``` 41 | 42 | ## Contributing 43 | 44 | Contributions are always welcome! 45 | 46 | ## License 47 | 48 | This software is provided free under [GNU GPL v3]. 49 | 50 | --- 51 | 52 | This project is supported by [Clan] and [Numtide](https://numtide.com/). 53 | 54 | ## Clan 55 | 56 | [Clan] is a peer-to-peer management framework based on [NixOS] that has: 57 | 58 | - a uniform interface 59 | - automated secret management 60 | - automated service setup 61 | - automated backups 62 | - peer-to-peer Mesh VPN 63 | 64 | Get in touch and learn more at: 65 | 66 | - [clan.lol](https://clan.lol) 67 | - [Matrix channel](https://matrix.to/#/#clan:clan.lol) for live discussions. 68 | - IRC bridge on [hackint#clan](https://chat.hackint.org/#/connect?join=clan) for real-time chat support. 69 | 70 | ## Numtide 71 | 72 | ![Numtide Logo](https://codahosted.io/docs/6FCIMTRM0p/blobs/bl-sgSunaXYWX/077f3f9d7d76d6a228a937afa0658292584dedb5b852a8ca370b6c61dabb7872b7f617e603f1793928dc5410c74b3e77af21a89e435fa71a681a868d21fd1f599dd10a647dd855e14043979f1df7956f67c3260c0442e24b34662307204b83ea34de929d) 73 | 74 | We’re a team of independent freelancers that love open source. 75 | We help our customers make their project lifecycles more efficient by: 76 | 77 | - Providing and supporting useful tools such as this one. 78 | - Building and deploying infrastructure, and offering dedicated DevOps support. 79 | - Building their in-house Nix skills, and integrating Nix with their workflows. 80 | - Developing additional features and tools. 81 | - Carrying out custom research and development. 82 | 83 | [Contact us](https://numtide.com/contact) if you have a project in mind, 84 | or if you need help with any of our supported tools, including this one. 85 | 86 | We'd love to hear from you. 87 | 88 | [Clan]: https://clan.lol 89 | [NixOS configurations]: https://nixos.org/manual/nixos/stable/#sec-configuration-syntax 90 | [NixOS Hardware]: https://github.com/NixOS/nixos-hardware 91 | [NixOS Facter Modules]: https://github.com/nix-community/nixos-facter-modules 92 | [NixOS modules]: https://github.com/nix-community/nixos-facter-modules 93 | [nixos-generate-config]: https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/installer/tools/nixos-generate-config.pl 94 | [Numtide Binary Cache]: https://numtide.cachix.org 95 | [nixos-facter]: https://github.com/nix-community/nixos-facter 96 | [nixpkgs]: https://github.com/nixos/nixpkgs 97 | [docs]: https://nix-community.github.io/nixos-facter 98 | [GNU GPL v3]: ./LICENSE 99 | [NixOS]: https://nixos.org 100 | -------------------------------------------------------------------------------- /pkg/linux/input/input_test.go: -------------------------------------------------------------------------------- 1 | //nolint:lll 2 | package input_test 3 | 4 | import ( 5 | "bytes" 6 | "io" 7 | "testing" 8 | 9 | "github.com/numtide/nixos-facter/pkg/linux/input" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | const ( 14 | devices = `I: Bus=0003 Vendor=1038 Product=1634 Version=0111 15 | N: Name="SteelSeries SteelSeries Apex 9 TKL" 16 | P: Phys=usb-0000:08:00.3-2.4.2/input2 17 | S: Sysfs=/devices/pci0000:00/0000:00:01.2/0000:02:00.0/0000:03:08.0/0000:08:00.3/usb3/3-2/3-2.4/3-2.4.2/3-2.4.2:1.2/0003:1038:1634.000B/input/input11 18 | U: Uniq= 19 | H: Handlers=sysrq kbd leds event4 20 | B: PROP=0 21 | B: EV=120013 22 | B: KEY=1000000000007 ff9f207ac14057ff febeffdfffefffff fffffffffffffffe 23 | B: MSC=10 24 | B: LED=7 25 | 26 | I: Bus=0003 Vendor=046d Product=407b Version=0111 27 | N: Name="Logitech MX Vertical" 28 | P: Phys=usb-0000:08:00.3-2.2/input2:1 29 | S: Sysfs=/devices/pci0000:00/0000:00:01.2/0000:02:00.0/0000:03:08.0/0000:08:00.3/usb3/3-2/3-2.2/3-2.2:1.2/0003:046D:C52B.0004/0003:046D:407B.0005/input/input8 30 | U: Uniq=5c-c6-5d-d4 31 | H: Handlers=sysrq kbd leds event1 mouse0 32 | B: PROP=0 33 | B: EV=12001f 34 | B: KEY=3f00033fff 0 0 483ffff17aff32d bfd4444600000000 ffff0001 130ff38b17d007 ffff7bfad9415fff ffbeffdfffefffff fffffffffffffffe 35 | B: REL=1943 36 | B: ABS=100000000 37 | B: MSC=10 38 | B: LED=1f 39 | 40 | I: Bus=0003 Vendor=b58e Product=9e84 Version=0100 41 | N: Name="Blue Microphones Yeti Stereo Microphone Consumer Control" 42 | P: Phys=usb-0000:0f:00.3-4/input3 43 | S: Sysfs=/devices/pci0000:00/0000:00:08.1/0000:0f:00.3/usb5/5-4/5-4:1.3/0003:B58E:9E84.0001/input/input0 44 | U: Uniq=797_2020/06/26_02565 45 | H: Handlers=kbd event0 46 | B: PROP=0 47 | B: EV=1b 48 | B: KEY=1 0 7800000000 e000000000000 0 49 | B: ABS=10000000000 50 | B: MSC=10 51 | 52 | ` 53 | ) 54 | 55 | func TestReadDevices(t *testing.T) { 56 | t.Parallel() 57 | as := require.New(t) 58 | r := io.NopCloser(bytes.NewReader([]byte(devices))) 59 | 60 | devices, err := input.ReadDevices(r) 61 | as.NoError(err) 62 | 63 | expected := []*input.Device{ 64 | { 65 | Bus: input.BusUsb, 66 | Vendor: uint16(0x1038), 67 | Product: uint16(0x1634), 68 | Version: uint16(0x0111), 69 | Name: "SteelSeries SteelSeries Apex 9 TKL", //nolint:dupword 70 | Phys: "usb-0000:08:00.3-2.4.2/input2", 71 | Sysfs: "/devices/pci0000:00/0000:00:01.2/0000:02:00.0/0000:03:08.0/0000:08:00.3/usb3/3-2/3-2.4/3-2.4.2/3-2.4.2:1.2/0003:1038:1634.000B/input/input11", 72 | Handlers: []string{"sysrq", "kbd", "leds", "event4"}, 73 | Capabilities: map[string]string{ 74 | "PROP": "0", 75 | "EV": "120013", 76 | "KEY": "1000000000007 ff9f207ac14057ff febeffdfffefffff fffffffffffffffe", 77 | "MSC": "10", 78 | "LED": "7", 79 | }, 80 | }, 81 | { 82 | Bus: input.BusUsb, 83 | Vendor: uint16(0x046d), 84 | Product: uint16(0x407b), 85 | Version: uint16(0x0111), 86 | Name: "Logitech MX Vertical", 87 | Phys: "usb-0000:08:00.3-2.2/input2:1", 88 | Sysfs: "/devices/pci0000:00/0000:00:01.2/0000:02:00.0/0000:03:08.0/0000:08:00.3/usb3/3-2/3-2.2/3-2.2:1.2/0003:046D:C52B.0004/0003:046D:407B.0005/input/input8", 89 | Handlers: []string{"sysrq", "kbd", "leds", "event1", "mouse0"}, 90 | Capabilities: map[string]string{ 91 | "PROP": "0", 92 | "EV": "12001f", 93 | "KEY": "3f00033fff 0 0 483ffff17aff32d bfd4444600000000 ffff0001 130ff38b17d007 ffff7bfad9415fff ffbeffdfffefffff fffffffffffffffe", 94 | "REL": "1943", 95 | "ABS": "100000000", 96 | "MSC": "10", 97 | "LED": "1f", 98 | }, 99 | }, 100 | { 101 | Bus: input.BusUsb, 102 | Vendor: uint16(0xb58e), 103 | Product: uint16(0x9e84), 104 | Version: uint16(0x0100), 105 | Name: "Blue Microphones Yeti Stereo Microphone Consumer Control", 106 | Phys: "usb-0000:0f:00.3-4/input3", 107 | Sysfs: "/devices/pci0000:00/0000:00:08.1/0000:0f:00.3/usb5/5-4/5-4:1.3/0003:B58E:9E84.0001/input/input0", 108 | Handlers: []string{"kbd", "event0"}, 109 | Capabilities: map[string]string{ 110 | "PROP": "0", 111 | "EV": "1b", 112 | "KEY": "1 0 7800000000 e000000000000 0", 113 | "ABS": "10000000000", 114 | "MSC": "10", 115 | }, 116 | }, 117 | } 118 | 119 | as.Equal(expected, devices) 120 | } 121 | -------------------------------------------------------------------------------- /pkg/hwinfo/hardware_enum_sub_class_mouse.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=SubClassMouse -json -transform=snake -trimprefix SubClassMouse -output=./hardware_enum_sub_class_mouse.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | _SubClassMouseName_0 = "ps2serbususbsun" 13 | _SubClassMouseLowerName_0 = "ps2serbususbsun" 14 | _SubClassMouseName_1 = "other" 15 | _SubClassMouseLowerName_1 = "other" 16 | ) 17 | 18 | var ( 19 | _SubClassMouseIndex_0 = [...]uint8{0, 3, 6, 9, 12, 15} 20 | _SubClassMouseIndex_1 = [...]uint8{0, 5} 21 | ) 22 | 23 | func (i SubClassMouse) String() string { 24 | switch { 25 | case 0 <= i && i <= 4: 26 | return _SubClassMouseName_0[_SubClassMouseIndex_0[i]:_SubClassMouseIndex_0[i+1]] 27 | case i == 128: 28 | return _SubClassMouseName_1 29 | default: 30 | return fmt.Sprintf("SubClassMouse(%d)", i) 31 | } 32 | } 33 | 34 | // An "invalid array index" compiler error signifies that the constant values have changed. 35 | // Re-run the stringer command to generate them again. 36 | func _SubClassMouseNoOp() { 37 | var x [1]struct{} 38 | _ = x[SubClassMousePs2-(0)] 39 | _ = x[SubClassMouseSer-(1)] 40 | _ = x[SubClassMouseBus-(2)] 41 | _ = x[SubClassMouseUsb-(3)] 42 | _ = x[SubClassMouseSun-(4)] 43 | _ = x[SubClassMouseOther-(128)] 44 | } 45 | 46 | var _SubClassMouseValues = []SubClassMouse{SubClassMousePs2, SubClassMouseSer, SubClassMouseBus, SubClassMouseUsb, SubClassMouseSun, SubClassMouseOther} 47 | 48 | var _SubClassMouseNameToValueMap = map[string]SubClassMouse{ 49 | _SubClassMouseName_0[0:3]: SubClassMousePs2, 50 | _SubClassMouseLowerName_0[0:3]: SubClassMousePs2, 51 | _SubClassMouseName_0[3:6]: SubClassMouseSer, 52 | _SubClassMouseLowerName_0[3:6]: SubClassMouseSer, 53 | _SubClassMouseName_0[6:9]: SubClassMouseBus, 54 | _SubClassMouseLowerName_0[6:9]: SubClassMouseBus, 55 | _SubClassMouseName_0[9:12]: SubClassMouseUsb, 56 | _SubClassMouseLowerName_0[9:12]: SubClassMouseUsb, 57 | _SubClassMouseName_0[12:15]: SubClassMouseSun, 58 | _SubClassMouseLowerName_0[12:15]: SubClassMouseSun, 59 | _SubClassMouseName_1[0:5]: SubClassMouseOther, 60 | _SubClassMouseLowerName_1[0:5]: SubClassMouseOther, 61 | } 62 | 63 | var _SubClassMouseNames = []string{ 64 | _SubClassMouseName_0[0:3], 65 | _SubClassMouseName_0[3:6], 66 | _SubClassMouseName_0[6:9], 67 | _SubClassMouseName_0[9:12], 68 | _SubClassMouseName_0[12:15], 69 | _SubClassMouseName_1[0:5], 70 | } 71 | 72 | // SubClassMouseString retrieves an enum value from the enum constants string name. 73 | // Throws an error if the param is not part of the enum. 74 | func SubClassMouseString(s string) (SubClassMouse, error) { 75 | if val, ok := _SubClassMouseNameToValueMap[s]; ok { 76 | return val, nil 77 | } 78 | 79 | if val, ok := _SubClassMouseNameToValueMap[strings.ToLower(s)]; ok { 80 | return val, nil 81 | } 82 | return 0, fmt.Errorf("%s does not belong to SubClassMouse values", s) 83 | } 84 | 85 | // SubClassMouseValues returns all values of the enum 86 | func SubClassMouseValues() []SubClassMouse { 87 | return _SubClassMouseValues 88 | } 89 | 90 | // SubClassMouseStrings returns a slice of all String values of the enum 91 | func SubClassMouseStrings() []string { 92 | strs := make([]string, len(_SubClassMouseNames)) 93 | copy(strs, _SubClassMouseNames) 94 | return strs 95 | } 96 | 97 | // IsASubClassMouse returns "true" if the value is listed in the enum definition. "false" otherwise 98 | func (i SubClassMouse) IsASubClassMouse() bool { 99 | for _, v := range _SubClassMouseValues { 100 | if i == v { 101 | return true 102 | } 103 | } 104 | return false 105 | } 106 | 107 | // MarshalJSON implements the json.Marshaler interface for SubClassMouse 108 | func (i SubClassMouse) MarshalJSON() ([]byte, error) { 109 | return json.Marshal(i.String()) 110 | } 111 | 112 | // UnmarshalJSON implements the json.Unmarshaler interface for SubClassMouse 113 | func (i *SubClassMouse) UnmarshalJSON(data []byte) error { 114 | var s string 115 | if err := json.Unmarshal(data, &s); err != nil { 116 | return fmt.Errorf("SubClassMouse should be a string, got %s", data) 117 | } 118 | 119 | var err error 120 | *i, err = SubClassMouseString(s) 121 | return err 122 | } 123 | -------------------------------------------------------------------------------- /docs/theme/home.html: -------------------------------------------------------------------------------- 1 | {% extends "main.html" %} {% block header %} 2 |
3 | 50 |
51 | {% endblock %} {% block hero %} 52 | 53 | 90 |
91 |
92 |
93 |
94 |

NixOS Facter

95 |

{{ config.site_description }}

96 | 101 | Get started 102 | 103 | 108 | Contribute 109 | 110 |
111 | 112 |
113 | 114 |
115 |
116 |
117 |
118 | {% endblock %} {% block tabs %}{% endblock %} {% block site_nav %}{% endblock %} 119 | {% block content %}{% endblock %} {% block footer %}{% endblock %} 120 | -------------------------------------------------------------------------------- /pkg/hwinfo/resource.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | 7 | // CGO cannot access union type fields, so we do this as a workaround 8 | hd_res_t *hd_res_next(hd_res_t *res) { return res->next; } 9 | hd_resource_types_t hd_res_get_type(hd_res_t *res) { return res->any.type; } 10 | res_any_t hd_res_get_any(hd_res_t *res) { return res->any; } 11 | */ 12 | import "C" 13 | 14 | import ( 15 | "bytes" 16 | "encoding/json" 17 | "errors" 18 | "fmt" 19 | "log" 20 | "slices" 21 | ) 22 | 23 | //go:generate enumer -type=ResourceType -json -transform=snake -trimprefix ResourceType -output=./resource_enum_type.go 24 | type ResourceType uint //nolint:recvcheck 25 | 26 | const ( 27 | ResourceTypeAny ResourceType = iota 28 | ResourceTypePhysMem 29 | ResourceTypeMem 30 | ResourceTypeIo 31 | ResourceTypeIrq 32 | ResourceTypeDma 33 | ResourceTypeMonitor 34 | 35 | ResourceTypeSize 36 | ResourceTypeDiskGeo 37 | ResourceTypeCache 38 | ResourceTypeBaud 39 | ResourceTypeInitStrings 40 | ResourceTypePppdOption 41 | 42 | ResourceTypeFramebuffer 43 | ResourceTypeHwaddr 44 | ResourceTypeLink 45 | ResourceTypeWlan 46 | ResourceTypeFc 47 | ResourceTypePhwaddr 48 | ) 49 | 50 | //nolint:ireturn 51 | func NewResource(res *C.hd_res_t) (Resource, error) { 52 | if res == nil { 53 | return nil, errors.New("resource is nil") 54 | } 55 | 56 | var ( 57 | err error 58 | result Resource 59 | ) 60 | 61 | resourceType := ResourceType(C.hd_res_get_type(res)) 62 | 63 | switch resourceType { 64 | case ResourceTypeFc: 65 | result, err = NewResourceFc(res, resourceType) 66 | case ResourceTypePhysMem: 67 | result, err = NewResourcePhysicalMemory(res, resourceType) 68 | case ResourceTypeMem: 69 | result, err = NewResourceMemory(res, resourceType) 70 | case ResourceTypeIo: 71 | result, err = NewResourceIO(res, resourceType) 72 | case ResourceTypeIrq: 73 | result, err = NewResourceIrq(res, resourceType) 74 | case ResourceTypeDma: 75 | result, err = NewResourceDma(res, resourceType) 76 | case ResourceTypeMonitor: 77 | result, err = NewResourceMonitor(res, resourceType) 78 | case ResourceTypeSize: 79 | result, err = NewResourceSize(res, resourceType) 80 | case ResourceTypeDiskGeo: 81 | result, err = NewResourceDiskGeo(res, resourceType) 82 | case ResourceTypeCache: 83 | result, err = NewResourceCache(res, resourceType) 84 | case ResourceTypeBaud: 85 | result, err = NewResourceBaud(res, resourceType) 86 | case ResourceTypeInitStrings: 87 | result, err = NewResourceInitStrings(res, resourceType) 88 | case ResourceTypePppdOption: 89 | result, err = NewResourcePppdOption(res, resourceType) 90 | case ResourceTypeFramebuffer: 91 | result, err = NewResourceFrameBuffer(res, resourceType) 92 | case ResourceTypeHwaddr, ResourceTypePhwaddr: 93 | result, err = NewResourceHardwareAddress(res, resourceType) 94 | case ResourceTypeLink: 95 | // this is the link status of a network interface and can change when we plug/unplug a cable 96 | case ResourceTypeWlan: 97 | result, err = NewResourceWlan(res, resourceType) 98 | case ResourceTypeAny: 99 | err = fmt.Errorf("unexpected resource type: %v", resourceType) 100 | } 101 | 102 | return result, err 103 | } 104 | 105 | func NewResources(hd *C.hd_t) ([]Resource, error) { 106 | var result []Resource 107 | 108 | for res := hd.res; res != nil; res = C.hd_res_next(res) { 109 | resource, err := NewResource(res) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | if resource == nil { 115 | continue 116 | } 117 | 118 | //nolint:exhaustive 119 | switch resource.ResourceType() { 120 | // these resources are not stable, so we filter them out 121 | case ResourceTypeMem, ResourceTypeIrq: 122 | continue 123 | 124 | default: 125 | result = append(result, resource) 126 | } 127 | } 128 | 129 | slices.SortFunc(result, func(a, b Resource) int { 130 | // We don't really care about a proper ordering for resources, just a stable sort that is reasonably quick. 131 | var err error 132 | 133 | jsonA, err := json.Marshal(a) 134 | if err != nil { 135 | log.Panicf("failed to marshal resource: %s", err) 136 | } 137 | 138 | jsonB, err := json.Marshal(b) 139 | if err != nil { 140 | log.Panicf("failed to marshal resource: %s", err) 141 | } 142 | 143 | return bytes.Compare(jsonA, jsonB) 144 | }) 145 | 146 | return result, nil 147 | } 148 | -------------------------------------------------------------------------------- /pkg/hwinfo/detail_pci.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "encoding/hex" 11 | "slices" 12 | "unsafe" 13 | ) 14 | 15 | //go:generate enumer -type=PciFlag -json -transform=snake -trimprefix PciFlag -output=./detail_enum_pci_flag.go 16 | type PciFlag uint //nolint:recvcheck 17 | 18 | const ( 19 | PciFlagOk PciFlag = iota 20 | PciFlagPm 21 | PciFlagAgp 22 | ) 23 | 24 | func ParsePciFlags(flags uint) []PciFlag { 25 | var result []PciFlag 26 | 27 | for _, flag := range PciFlagValues() { 28 | if (flag & (1 << flags)) == 1 { 29 | result = append(result, flag) 30 | } 31 | } 32 | // ensure stable output 33 | slices.Sort(result) 34 | 35 | return result 36 | } 37 | 38 | type DetailPci struct { 39 | Type DetailType `json:"-"` 40 | 41 | Flags []PciFlag `json:"flags,omitempty"` // 42 | Function uint32 `json:"function"` 43 | 44 | // todo map pci constants from pci.h? 45 | Command uint32 `json:"command"` // PCI_COMMAND 46 | HeaderType uint32 `json:"header_type"` // PCI_HEADER_TYPE 47 | SecondaryBus uint32 `json:"secondary_bus"` // > 0 for PCI & CB bridges 48 | 49 | Irq uint16 `json:"-"` // used irq if any 50 | // Programming Interface Byte: a read-only register that specifies a register-level programming interface for the 51 | // device. 52 | ProgIf uint16 `json:"prog_if"` 53 | 54 | // already included in the parent model, so we omit from JSON output 55 | Bus uint `json:"-"` 56 | Slot uint `json:"-"` 57 | 58 | BaseClass uint `json:"-"` 59 | SubClass uint `json:"-"` 60 | 61 | Device uint `json:"-"` 62 | Vendor uint `json:"-"` 63 | SubDevice uint `json:"-"` 64 | SubVendor uint `json:"-"` 65 | Revision uint `json:"-"` 66 | 67 | BaseAddress [7]uint64 `json:"-"` // I/O or memory base 68 | BaseLength [7]uint64 `json:"-"` // I/O or memory ranges 69 | AddressFlags [7]uint `json:"-"` // I/O or memory address flags 70 | 71 | RomBaseAddress uint64 `json:"-"` // memory base for card ROM 72 | RomBaseLength uint64 `json:"-"` // memory range for card ROM 73 | 74 | SysfsID string `json:"-"` // sysfs path 75 | SysfsBusID string `json:"-"` // sysfs bus id 76 | ModuleAlias string `json:"-"` // module alias 77 | Label string `json:"-"` // Consistent Device Name (CDN), pci firmware 3.1, chapter 4.6.7 78 | 79 | // Omit from JSON output 80 | Data string `json:"-"` // the PCI data, hex encoded 81 | DataLength uint `json:"-"` // holds the actual length of Data 82 | DataExtLength uint `json:"-"` // max. accessed config byte 83 | Log string `json:"-"` // log messages 84 | } 85 | 86 | func (p DetailPci) DetailType() DetailType { 87 | return DetailTypePci 88 | } 89 | 90 | func NewDetailPci(pci C.hd_detail_pci_t) (*DetailPci, error) { 91 | data := pci.data 92 | 93 | return &DetailPci{ 94 | Type: DetailTypePci, 95 | Data: hex.EncodeToString(C.GoBytes(unsafe.Pointer(&data.data), 256)), //nolint:gocritic 96 | DataLength: uint(data.data_len), 97 | DataExtLength: uint(data.data_ext_len), 98 | Log: C.GoString(data.log), 99 | Flags: ParsePciFlags(uint(data.flags)), 100 | Command: uint32(data.cmd), 101 | HeaderType: uint32(data.hdr_type), 102 | SecondaryBus: uint32(data.secondary_bus), 103 | Bus: uint(data.bus), 104 | Slot: uint(data.slot), 105 | Function: uint32(data._func), 106 | BaseClass: uint(data.base_class), 107 | SubClass: uint(data.sub_class), 108 | ProgIf: uint16(data.prog_if), 109 | Device: uint(data.dev), 110 | Vendor: uint(data.vend), 111 | SubDevice: uint(data.sub_dev), 112 | SubVendor: uint(data.sub_vend), 113 | Revision: uint(data.rev), 114 | Irq: uint16(data.irq), 115 | BaseAddress: [7]uint64(ReadUint64Array(unsafe.Pointer(&data.base_addr), 7)), 116 | BaseLength: [7]uint64(ReadUint64Array(unsafe.Pointer(&data.base_len), 7)), 117 | AddressFlags: [7]uint(ReadUintArray(unsafe.Pointer(&data.addr_flags), 7)), 118 | RomBaseAddress: uint64(data.rom_base_addr), 119 | RomBaseLength: uint64(data.rom_base_len), 120 | SysfsID: C.GoString(data.sysfs_id), 121 | SysfsBusID: C.GoString(data.sysfs_bus_id), 122 | ModuleAlias: C.GoString(data.modalias), 123 | Label: C.GoString(data.label), 124 | }, nil 125 | } 126 | -------------------------------------------------------------------------------- /pkg/hwinfo/detail_usb.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | /* 4 | #cgo pkg-config: hwinfo 5 | #include 6 | */ 7 | import "C" 8 | 9 | //go:generate enumer -type=UsbClass -json -transform=snake -trimprefix UsbClass -output=./detail_usb_enum_usb_class.go 10 | type UsbClass uint16 //nolint:recvcheck 11 | 12 | const ( 13 | UsbClassPerInterface UsbClass = 0x00 14 | UsbClassAudio UsbClass = 0x01 15 | UsbClassComm UsbClass = 0x02 16 | UsbClassHID UsbClass = 0x03 17 | UsbClassPhysical UsbClass = 0x05 18 | UsbClassImage UsbClass = 0x06 19 | UsbClassPTP UsbClass = UsbClassImage // legacy name for image 20 | UsbClassPrinter UsbClass = 0x07 21 | UsbClassMassStorage UsbClass = 0x08 22 | UsbClassHub UsbClass = 0x09 23 | UsbClassData UsbClass = 0x0a 24 | UsbClassSmartCard UsbClass = 0x0b 25 | UsbClassContentSecurity UsbClass = 0x0d 26 | UsbClassVideo UsbClass = 0x0e 27 | UsbClassPersonalHealthcare UsbClass = 0x0f 28 | UsbClassAudioVideo UsbClass = 0x10 29 | UsbClassBillboard UsbClass = 0x11 30 | UsbClassUSBTypeCBridge UsbClass = 0x12 31 | UsbClassDiagnosticDevice UsbClass = 0xdc 32 | UsbClassWireless UsbClass = 0xe0 33 | UsbClassMiscellaneous UsbClass = 0xef 34 | UsbClassApplication UsbClass = 0xfe 35 | UsbClassVendorSpec UsbClass = 0xff 36 | ) 37 | 38 | type DetailUsb struct { 39 | Type DetailType `json:"-"` 40 | 41 | DeviceClass ID `json:"device_class"` 42 | DeviceSubclass ID `json:"device_subclass"` 43 | DeviceProtocol int `json:"device_protocol"` 44 | 45 | InterfaceClass ID `json:"interface_class"` 46 | InterfaceSubclass ID `json:"interface_subclass"` 47 | InterfaceProtocol int `json:"interface_protocol"` 48 | InterfaceNumber int `json:"interface_number"` 49 | InterfaceAlternateSetting int `json:"interface_alternate_setting"` 50 | 51 | InterfaceAssociation *DetailUsbInterfaceAssociation `json:"interface_association,omitempty"` 52 | } 53 | 54 | type DetailUsbInterfaceAssociation struct { 55 | FunctionClass ID `json:"function_class"` 56 | FunctionSubclass ID `json:"function_subclass"` 57 | FunctionProtocol int `json:"function_protocol"` 58 | InterfaceCount int `json:"interface_count"` 59 | FirstInterface int `json:"first_interface"` 60 | } 61 | 62 | func (d DetailUsb) DetailType() DetailType { 63 | return DetailTypeUsb 64 | } 65 | 66 | func NewDetailUsb(usb C.hd_detail_usb_t) (*DetailUsb, error) { 67 | data := usb.data 68 | 69 | if data.next != nil { 70 | println("usb next is not nil") 71 | } 72 | 73 | detail := &DetailUsb{ 74 | Type: DetailTypeUsb, 75 | DeviceClass: ID{ 76 | Type: IDTagUsb, 77 | Value: uint16(data.d_cls), 78 | Name: UsbClass(data.d_cls).String(), 79 | }, 80 | DeviceSubclass: ID{ 81 | Type: IDTagUsb, 82 | Value: uint16(data.d_sub), 83 | Name: UsbClass(data.d_sub).String(), 84 | }, 85 | DeviceProtocol: int(data.d_prot), 86 | InterfaceClass: ID{ 87 | Type: IDTagUsb, 88 | Value: uint16(data.i_cls), 89 | Name: UsbClass(data.i_cls).String(), 90 | }, 91 | InterfaceSubclass: ID{ 92 | Type: IDTagUsb, 93 | Value: uint16(data.i_sub), 94 | Name: UsbClass(data.i_sub).String(), 95 | }, 96 | InterfaceProtocol: int(data.i_prot), 97 | InterfaceNumber: int(data.ifdescr), 98 | InterfaceAlternateSetting: int(data.i_alt), 99 | } 100 | 101 | // The Interface Association Descriptor groups multiple interfaces that are part of a single functional device. 102 | // For instance, a USB webcam with an integrated microphone would use an IAD to group the video input interface and 103 | // the audio input interface together. 104 | if data.iad_i_count > 0 { 105 | detail.InterfaceAssociation = &DetailUsbInterfaceAssociation{ 106 | FunctionClass: ID{ 107 | Type: IDTagUsb, 108 | Value: uint16(data.iad_f_cls), 109 | Name: UsbClass(data.iad_f_cls).String(), 110 | }, 111 | FunctionSubclass: ID{ 112 | Type: IDTagUsb, 113 | Value: uint16(data.iad_f_sub), 114 | Name: UsbClass(data.iad_f_sub).String(), 115 | }, 116 | FunctionProtocol: int(data.iad_f_prot), 117 | FirstInterface: int(data.iad_i_first), 118 | InterfaceCount: int(data.iad_i_count), 119 | } 120 | } 121 | 122 | return detail, nil 123 | } 124 | -------------------------------------------------------------------------------- /pkg/hwinfo/input.go: -------------------------------------------------------------------------------- 1 | package hwinfo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log/slog" 7 | 8 | "github.com/numtide/nixos-facter/pkg/linux/input" 9 | "github.com/numtide/nixos-facter/pkg/udev" 10 | ) 11 | 12 | // captureTouchpads scans the input devices and identifies touchpads, returning a slice of HardwareDevice structs or an 13 | // error. 14 | // It accepts a deviceIdx to ensure it continues on from the last device index generated by hwinfo. 15 | func captureTouchpads(deviceIdx uint16) ([]HardwareDevice, error) { 16 | inputDevices, err := input.ReadDevices(nil) 17 | if err != nil { 18 | return nil, fmt.Errorf("failed to read input devices: %w", err) 19 | } 20 | 21 | var result []HardwareDevice //nolint:prealloc 22 | 23 | for _, inputDevice := range inputDevices { 24 | path := "/sys" + inputDevice.Sysfs 25 | 26 | udevData, err := udev.Read(path) 27 | if errors.Is(err, udev.ErrNotFound) { 28 | slog.Warn("udev data not found", "name", inputDevice.Name, "sysfs", inputDevice.Sysfs) 29 | continue 30 | } else if err != nil { 31 | return nil, fmt.Errorf("failed to fetch udev data for device %q with udev data: %w", path, err) 32 | } 33 | 34 | if udevData.Input == nil { 35 | slog.Debug("udev data missing input", "name", inputDevice.Name, "sysfs", inputDevice.Sysfs) 36 | continue 37 | } 38 | 39 | if !udevData.Input.IsTouchpad { 40 | // currently, we are only interested in touchpads, as hwinfo does not capture them 41 | // eventually, we may take over more of the input processing that hwinfo performs 42 | continue 43 | } 44 | 45 | if len(inputDevice.Handlers) == 0 { 46 | // I believe this shouldn't be possible, and if it does occur, we should error 47 | return nil, fmt.Errorf("no handlers found for input device %s", inputDevice.Sysfs) 48 | } 49 | 50 | // create a hardware entry for the report 51 | hd := HardwareDevice{ 52 | // todo AttachedTo: it's unclear how to work this out 53 | Class: HardwareClassMouse, 54 | BaseClass: NewBaseClassID(BaseClassTouchpad), 55 | Vendor: &ID{ 56 | Name: udevData.Vendor, 57 | Value: inputDevice.Vendor, 58 | }, 59 | Device: &ID{ 60 | Name: udevData.Model, 61 | Value: inputDevice.Product, 62 | }, 63 | SysfsID: inputDevice.Sysfs, 64 | } 65 | 66 | switch inputDevice.Bus { 67 | case input.BusI2c: 68 | hd.BusType = NewBusID(BusSerial) 69 | hd.SubClass = &ID{ 70 | Name: SubClassMouseBus.String(), 71 | Value: uint16(SubClassMouseSer), 72 | } 73 | 74 | case input.BusUsb: 75 | hd.BusType = NewBusID(BusUsb) 76 | hd.SubClass = &ID{ 77 | Name: SubClassMouseUsb.String(), 78 | Value: uint16(SubClassMouseUsb), 79 | } 80 | case input.BusI8042: 81 | hd.BusType = NewBusID(BusPs2) 82 | hd.SubClass = &ID{ 83 | Name: SubClassMousePs2.String(), 84 | Value: uint16(SubClassMousePs2), 85 | } 86 | 87 | case input.BusRmi: 88 | // RMI is a protocol which runs over other physical buses, typically i2c, but it can also be over others 89 | // such as USB or SPI. 90 | // I'm not sure how to map this into hwinfo's bus classification, so for now we will use other for both the 91 | // bus and mouse subclass. 92 | hd.BusType = NewBusID(BusOther) 93 | hd.SubClass = &ID{ 94 | Name: SubClassMouseOther.String(), 95 | Value: uint16(SubClassMouseOther), 96 | } 97 | 98 | case input.BusPci, 99 | input.BusIsapnp, 100 | input.BusHil, 101 | input.BusBluetooth, 102 | input.BusVirtual, 103 | input.BusIsa, 104 | input.BusXtkbd, 105 | input.BusRs232, 106 | input.BusGameport, 107 | input.BusParport, 108 | input.BusAmiga, 109 | input.BusAdb, 110 | input.BusHost, 111 | input.BusGsc, 112 | input.BusAtari, 113 | input.BusSpi, 114 | input.BusCec, 115 | input.BusIntelIshtp, 116 | input.BusAmdSfh: 117 | // todo unsure if touchpads can be on any other bus type 118 | return nil, fmt.Errorf("unsupported bus type: %s", inputDevice.Bus) 119 | } 120 | 121 | // todo should we error if no event handler is found? 122 | if handler := inputDevice.EventHandler(); handler != "" { 123 | hd.UnixDeviceNames = append(hd.UnixDeviceNames, "/dev/input/"+handler) 124 | } 125 | 126 | if handler := inputDevice.MouseHandler(); handler != "" { 127 | hd.UnixDeviceNames = append(hd.UnixDeviceNames, "/dev/input/ + handler") 128 | } 129 | 130 | hd.Index = deviceIdx 131 | result = append(result, hd) 132 | 133 | deviceIdx++ 134 | } 135 | 136 | return result, nil 137 | } 138 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "blueprint": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "systems": [ 9 | "systems" 10 | ] 11 | }, 12 | "locked": { 13 | "lastModified": 1763308703, 14 | "narHash": "sha256-O9Y+Wer8wOh+N+4kcCK5p/VLrXyX+ktk0/s3HdZvJzk=", 15 | "owner": "numtide", 16 | "repo": "blueprint", 17 | "rev": "5a9bba070f801d63e2af3c9ef00b86b212429f4f", 18 | "type": "github" 19 | }, 20 | "original": { 21 | "owner": "numtide", 22 | "repo": "blueprint", 23 | "type": "github" 24 | } 25 | }, 26 | "disko": { 27 | "inputs": { 28 | "nixpkgs": [ 29 | "nixpkgs" 30 | ] 31 | }, 32 | "locked": { 33 | "lastModified": 1762276996, 34 | "narHash": "sha256-TtcPgPmp2f0FAnc+DMEw4ardEgv1SGNR3/WFGH0N19M=", 35 | "owner": "nix-community", 36 | "repo": "disko", 37 | "rev": "af087d076d3860760b3323f6b583f4d828c1ac17", 38 | "type": "github" 39 | }, 40 | "original": { 41 | "owner": "nix-community", 42 | "repo": "disko", 43 | "type": "github" 44 | } 45 | }, 46 | "flake-utils": { 47 | "inputs": { 48 | "systems": [ 49 | "systems" 50 | ] 51 | }, 52 | "locked": { 53 | "lastModified": 1731533236, 54 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 55 | "owner": "numtide", 56 | "repo": "flake-utils", 57 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 58 | "type": "github" 59 | }, 60 | "original": { 61 | "owner": "numtide", 62 | "repo": "flake-utils", 63 | "type": "github" 64 | } 65 | }, 66 | "hwinfo": { 67 | "inputs": { 68 | "blueprint": [ 69 | "blueprint" 70 | ], 71 | "nixpkgs": [ 72 | "nixpkgs" 73 | ], 74 | "systems": [ 75 | "systems" 76 | ] 77 | }, 78 | "locked": { 79 | "lastModified": 1762279079, 80 | "narHash": "sha256-GL3fNCSaU45fNihEksgtPtbuLkc+tVGXtPH05wbrHwI=", 81 | "owner": "numtide", 82 | "repo": "hwinfo", 83 | "rev": "bfeab0b4e38b200c7a62a44d4d01601a86fe1091", 84 | "type": "github" 85 | }, 86 | "original": { 87 | "owner": "numtide", 88 | "repo": "hwinfo", 89 | "type": "github" 90 | } 91 | }, 92 | "nixpkgs": { 93 | "locked": { 94 | "lastModified": 1766314097, 95 | "narHash": "sha256-laJftWbghBehazn/zxVJ8NdENVgjccsWAdAqKXhErrM=", 96 | "ref": "nixpkgs-unstable", 97 | "rev": "306ea70f9eb0fb4e040f8540e2deab32ed7e2055", 98 | "shallow": true, 99 | "type": "git", 100 | "url": "https://github.com/NixOS/nixpkgs" 101 | }, 102 | "original": { 103 | "ref": "nixpkgs-unstable", 104 | "shallow": true, 105 | "type": "git", 106 | "url": "https://github.com/NixOS/nixpkgs" 107 | } 108 | }, 109 | "root": { 110 | "inputs": { 111 | "blueprint": "blueprint", 112 | "disko": "disko", 113 | "flake-utils": "flake-utils", 114 | "hwinfo": "hwinfo", 115 | "nixpkgs": "nixpkgs", 116 | "systems": "systems", 117 | "treefmt-nix": "treefmt-nix" 118 | } 119 | }, 120 | "systems": { 121 | "locked": { 122 | "lastModified": 1681028828, 123 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 124 | "owner": "nix-systems", 125 | "repo": "default", 126 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 127 | "type": "github" 128 | }, 129 | "original": { 130 | "owner": "nix-systems", 131 | "repo": "default", 132 | "type": "github" 133 | } 134 | }, 135 | "treefmt-nix": { 136 | "inputs": { 137 | "nixpkgs": [ 138 | "nixpkgs" 139 | ] 140 | }, 141 | "locked": { 142 | "lastModified": 1766000401, 143 | "narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=", 144 | "owner": "numtide", 145 | "repo": "treefmt-nix", 146 | "rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd", 147 | "type": "github" 148 | }, 149 | "original": { 150 | "owner": "numtide", 151 | "repo": "treefmt-nix", 152 | "type": "github" 153 | } 154 | } 155 | }, 156 | "root": "root", 157 | "version": 7 158 | } 159 | -------------------------------------------------------------------------------- /pkg/hwinfo/driver_info_enum_type.go: -------------------------------------------------------------------------------- 1 | // Code generated by "enumer -type=DriverInfoType -json -transform=snake -trimprefix DriverInfoType -output=./driver_info_enum_type.go"; DO NOT EDIT. 2 | 3 | package hwinfo 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const _DriverInfoTypeName = "anydisplaymodulemousex11isdnkeyboarddsl" 12 | 13 | var _DriverInfoTypeIndex = [...]uint8{0, 3, 10, 16, 21, 24, 28, 36, 39} 14 | 15 | const _DriverInfoTypeLowerName = "anydisplaymodulemousex11isdnkeyboarddsl" 16 | 17 | func (i DriverInfoType) String() string { 18 | if i >= DriverInfoType(len(_DriverInfoTypeIndex)-1) { 19 | return fmt.Sprintf("DriverInfoType(%d)", i) 20 | } 21 | return _DriverInfoTypeName[_DriverInfoTypeIndex[i]:_DriverInfoTypeIndex[i+1]] 22 | } 23 | 24 | // An "invalid array index" compiler error signifies that the constant values have changed. 25 | // Re-run the stringer command to generate them again. 26 | func _DriverInfoTypeNoOp() { 27 | var x [1]struct{} 28 | _ = x[DriverInfoTypeAny-(0)] 29 | _ = x[DriverInfoTypeDisplay-(1)] 30 | _ = x[DriverInfoTypeModule-(2)] 31 | _ = x[DriverInfoTypeMouse-(3)] 32 | _ = x[DriverInfoTypeX11-(4)] 33 | _ = x[DriverInfoTypeIsdn-(5)] 34 | _ = x[DriverInfoTypeKeyboard-(6)] 35 | _ = x[DriverInfoTypeDsl-(7)] 36 | } 37 | 38 | var _DriverInfoTypeValues = []DriverInfoType{DriverInfoTypeAny, DriverInfoTypeDisplay, DriverInfoTypeModule, DriverInfoTypeMouse, DriverInfoTypeX11, DriverInfoTypeIsdn, DriverInfoTypeKeyboard, DriverInfoTypeDsl} 39 | 40 | var _DriverInfoTypeNameToValueMap = map[string]DriverInfoType{ 41 | _DriverInfoTypeName[0:3]: DriverInfoTypeAny, 42 | _DriverInfoTypeLowerName[0:3]: DriverInfoTypeAny, 43 | _DriverInfoTypeName[3:10]: DriverInfoTypeDisplay, 44 | _DriverInfoTypeLowerName[3:10]: DriverInfoTypeDisplay, 45 | _DriverInfoTypeName[10:16]: DriverInfoTypeModule, 46 | _DriverInfoTypeLowerName[10:16]: DriverInfoTypeModule, 47 | _DriverInfoTypeName[16:21]: DriverInfoTypeMouse, 48 | _DriverInfoTypeLowerName[16:21]: DriverInfoTypeMouse, 49 | _DriverInfoTypeName[21:24]: DriverInfoTypeX11, 50 | _DriverInfoTypeLowerName[21:24]: DriverInfoTypeX11, 51 | _DriverInfoTypeName[24:28]: DriverInfoTypeIsdn, 52 | _DriverInfoTypeLowerName[24:28]: DriverInfoTypeIsdn, 53 | _DriverInfoTypeName[28:36]: DriverInfoTypeKeyboard, 54 | _DriverInfoTypeLowerName[28:36]: DriverInfoTypeKeyboard, 55 | _DriverInfoTypeName[36:39]: DriverInfoTypeDsl, 56 | _DriverInfoTypeLowerName[36:39]: DriverInfoTypeDsl, 57 | } 58 | 59 | var _DriverInfoTypeNames = []string{ 60 | _DriverInfoTypeName[0:3], 61 | _DriverInfoTypeName[3:10], 62 | _DriverInfoTypeName[10:16], 63 | _DriverInfoTypeName[16:21], 64 | _DriverInfoTypeName[21:24], 65 | _DriverInfoTypeName[24:28], 66 | _DriverInfoTypeName[28:36], 67 | _DriverInfoTypeName[36:39], 68 | } 69 | 70 | // DriverInfoTypeString retrieves an enum value from the enum constants string name. 71 | // Throws an error if the param is not part of the enum. 72 | func DriverInfoTypeString(s string) (DriverInfoType, error) { 73 | if val, ok := _DriverInfoTypeNameToValueMap[s]; ok { 74 | return val, nil 75 | } 76 | 77 | if val, ok := _DriverInfoTypeNameToValueMap[strings.ToLower(s)]; ok { 78 | return val, nil 79 | } 80 | return 0, fmt.Errorf("%s does not belong to DriverInfoType values", s) 81 | } 82 | 83 | // DriverInfoTypeValues returns all values of the enum 84 | func DriverInfoTypeValues() []DriverInfoType { 85 | return _DriverInfoTypeValues 86 | } 87 | 88 | // DriverInfoTypeStrings returns a slice of all String values of the enum 89 | func DriverInfoTypeStrings() []string { 90 | strs := make([]string, len(_DriverInfoTypeNames)) 91 | copy(strs, _DriverInfoTypeNames) 92 | return strs 93 | } 94 | 95 | // IsADriverInfoType returns "true" if the value is listed in the enum definition. "false" otherwise 96 | func (i DriverInfoType) IsADriverInfoType() bool { 97 | for _, v := range _DriverInfoTypeValues { 98 | if i == v { 99 | return true 100 | } 101 | } 102 | return false 103 | } 104 | 105 | // MarshalJSON implements the json.Marshaler interface for DriverInfoType 106 | func (i DriverInfoType) MarshalJSON() ([]byte, error) { 107 | return json.Marshal(i.String()) 108 | } 109 | 110 | // UnmarshalJSON implements the json.Unmarshaler interface for DriverInfoType 111 | func (i *DriverInfoType) UnmarshalJSON(data []byte) error { 112 | var s string 113 | if err := json.Unmarshal(data, &s); err != nil { 114 | return fmt.Errorf("DriverInfoType should be a string, got %s", data) 115 | } 116 | 117 | var err error 118 | *i, err = DriverInfoTypeString(s) 119 | return err 120 | } 121 | --------------------------------------------------------------------------------