├── .envrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug.yml
│ └── feature_request.yml
└── workflows
│ ├── build-and-test.yml
│ ├── release.yml
│ ├── site.yml
│ └── update-flake.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── apply
│ └── apply.go
├── completion
│ └── completion.go
├── enter
│ └── enter.go
├── features
│ └── features.go
├── generation
│ ├── delete
│ │ ├── delete.go
│ │ ├── resolver.go
│ │ └── resolver_test.go
│ ├── diff
│ │ └── diff.go
│ ├── generation.go
│ ├── list
│ │ ├── list.go
│ │ └── tui.go
│ ├── rollback
│ │ └── rollback.go
│ ├── shared
│ │ └── utils.go
│ └── switch
│ │ └── switch.go
├── info
│ └── info.go
├── init
│ ├── configuration.nix.txt
│ ├── cpuinfo.go
│ ├── devices.go
│ ├── filesystems.go
│ ├── flake.nix.txt
│ ├── generate.go
│ ├── hardware_configuration.nix.txt
│ └── init.go
├── install
│ └── install.go
├── manual
│ └── manual.go
├── option
│ ├── cache.go
│ ├── completion.go
│ └── option.go
├── repl
│ └── repl.go
└── root
│ ├── aliases.go
│ └── root.go
├── default.nix
├── doc
├── .gitignore
├── book.toml
├── build.go
├── man
│ ├── nixos-cli-apply.1.scd
│ ├── nixos-cli-enter.1.scd
│ ├── nixos-cli-env.5.scd
│ ├── nixos-cli-features.1.scd
│ ├── nixos-cli-generation-delete.1.scd
│ ├── nixos-cli-generation-diff.1.scd
│ ├── nixos-cli-generation-list.1.scd
│ ├── nixos-cli-generation-rollback.1.scd
│ ├── nixos-cli-generation-switch.1.scd
│ ├── nixos-cli-generation.1.scd
│ ├── nixos-cli-info.1.scd
│ ├── nixos-cli-init.1.scd
│ ├── nixos-cli-install.1.scd
│ ├── nixos-cli-manual.1.scd
│ ├── nixos-cli-option-tui.1.scd
│ ├── nixos-cli-option.1.scd
│ ├── nixos-cli-repl.1.scd
│ ├── nixos-cli-settings.5.scd.template
│ └── nixos-cli.1.scd
└── src
│ ├── SUMMARY.md
│ ├── commands.md
│ ├── contributing.md
│ ├── faq.md
│ ├── installation.md
│ ├── introduction.md
│ ├── module.md
│ ├── overview.md
│ ├── roadmap.md
│ └── settings.md
├── flake.lock
├── flake.nix
├── go.mod
├── go.sum
├── internal
├── activation
│ └── activation.go
├── build
│ └── vars.go
├── cmd
│ ├── errors
│ │ └── errors.go
│ ├── nixopts
│ │ ├── convert.go
│ │ ├── convert_test.go
│ │ └── nixopts.go
│ ├── opts
│ │ └── opts.go
│ └── utils
│ │ ├── confirmation.go
│ │ └── utils.go
├── configuration
│ ├── configuration.go
│ ├── configuration_test.go
│ ├── flake.go
│ └── legacy.go
├── constants
│ └── constants.go
├── generation
│ ├── completion.go
│ ├── diff.go
│ ├── generation.go
│ └── specialisations.go
├── logger
│ ├── context.go
│ └── logger.go
├── settings
│ ├── completion.go
│ ├── completion_test.go
│ ├── context.go
│ ├── errors.go
│ ├── settings.go
│ └── settings_test.go
├── system
│ ├── local.go
│ ├── runner.go
│ └── system.go
├── time
│ ├── systemd.go
│ └── systemd_test.go
└── utils
│ └── utils.go
├── main.go
├── module.nix
├── package.nix
└── shell.nix
/.envrc:
--------------------------------------------------------------------------------
1 | use flake || use nix
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: File a bug report
3 | labels: ['bug']
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | A bug is when something (a feature, behavior, etc.) does not work in a
9 | way that is otherwise expected.
10 |
11 | ### Look through existing issues before filing!
12 |
13 | Please make an effort to look through the issue tracker before filing
14 | any bugs. Duplicates only create more work when triaging.
15 | - type: textarea
16 | id: what-happened
17 | attributes:
18 | label: What Happened?
19 | description: 'Explain what happened, in as much detail as necessary.'
20 | placeholder:
21 | 'I encountered a bug, and this is what happened, in good detail.'
22 | validations:
23 | required: true
24 | - type: textarea
25 | id: reproduction
26 | attributes:
27 | label: How To Reproduce
28 | description: |
29 | How can one reproduce this bug? Include all relevant information, such as:
30 |
31 | - Logs (ideally ran with `--verbose` if applicable)
32 | - Operating system (i.e. the output from `nixos info -m`)
33 | - Potentially problematic env variables
34 | placeholder: |
35 | This is how to reproduce it. I am running NixOS 24.11 (Vicuna) on the stable branch.
36 | validations:
37 | required: true
38 | - type: textarea
39 | id: expected
40 | attributes:
41 | label: Expected Behavior
42 | description: 'What behavior was expected to occur?'
43 | placeholder:
44 | 'I expected to be able to run ___ without a segmentation fault.'
45 | validations:
46 | required: true
47 | - type: textarea
48 | id: compiled-features
49 | attributes:
50 | label: Features
51 | description: |
52 | Please run `nixos features` and paste the output here.
53 | This will automatically formatted into code output.
54 | render: shell
55 | validations:
56 | required: true
57 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description:
3 | Make a request for new functionality or enhancements to existing features
4 | labels: ['enhancement']
5 | body:
6 | - type: textarea
7 | id: feature-description
8 | attributes:
9 | label: Feature Description
10 | description: |
11 | A concise description of what functionality you would like to see.
12 |
13 | Include details such as what problems this feature will solve. Keep
14 | in mind the following food for thought:
15 |
16 | - Are there existing tools that can do this? If so, how will this tool be able
17 | to do this better?
18 | - How complex of a feature will this be?
19 | - How can we make sure that this does not contribute to feature creep? Keeping
20 | ideas within the scope of this tool is **extremely** important.
21 | placeholder: 'I want a TUI menu for searching through NixOS options.'
22 | validations:
23 | required: true
24 | - type: dropdown
25 | id: help
26 | attributes:
27 | label: 'Help'
28 | description:
29 | 'Would you be able to implement this by submitting a pull request?'
30 | options:
31 | - 'Yes'
32 | - "Yes, but I don't know how to start; I would need guidance"
33 | - 'No'
34 | validations:
35 | required: true
36 | - type: checkboxes
37 | id: looked-through-existing-requests
38 | attributes:
39 | label: Issues
40 | options:
41 | - label:
42 | I have checked [existing
43 | issues](https://github.com/water-sucks/nixos/issues?q=is%3Aissue)
44 | and there are no existing ones with the same request.
45 | required: true
46 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-test.yml:
--------------------------------------------------------------------------------
1 | name: Build/Test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | workflow_dispatch:
10 |
11 | jobs:
12 | build-and-test:
13 | name: Build/Test
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v4
18 |
19 | - name: Install Nix
20 | uses: DeterminateSystems/nix-installer-action@main
21 |
22 | - name: Initialize Cachix
23 | uses: cachix/cachix-action@v14
24 | with:
25 | name: watersucks
26 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
27 | skipPush: true
28 |
29 | - name: Check for compile errors
30 | run: |
31 | nix develop .# -c make
32 | nix develop .# -c make FLAKE=false
33 |
34 | - name: Run tests
35 | run: nix develop .# -c make test
36 |
37 | - name: Build Nix packages
38 | run: nix build .#{nixos,nixosLegacy}
39 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | git-ref:
6 | description: 'Git ref to publish to Cachix'
7 | required: true
8 | type: string
9 | release:
10 | types: [created]
11 |
12 | jobs:
13 | publish-to-cachix:
14 | name: Publish to Cache
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v4
19 | with:
20 | ref: ${{ inputs.git-ref }}
21 |
22 | - name: Install Nix
23 | uses: DeterminateSystems/nix-installer-action@main
24 |
25 | - name: Initialize Cachix
26 | uses: cachix/cachix-action@v14
27 | with:
28 | name: watersucks
29 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
30 |
31 | - name: Build Nix packages
32 | run: nix build .#{nixos,nixosLegacy}
33 |
--------------------------------------------------------------------------------
/.github/workflows/site.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Website
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: write
12 | pages: write
13 | id-token: write
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Install Nix
21 | uses: DeterminateSystems/nix-installer-action@main
22 |
23 | - name: Initialize Cachix
24 | uses: cachix/cachix-action@v14
25 | with:
26 | name: watersucks
27 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
28 | skipPush: true
29 |
30 | - name: Build Book
31 | run: |
32 | nix develop .# -c make site
33 |
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v4
36 |
37 | - name: Upload artifact
38 | uses: actions/upload-pages-artifact@v3
39 | with:
40 | path: 'site'
41 |
42 | - name: Deploy to GitHub Pages
43 | id: deployment
44 | uses: actions/deploy-pages@v4
45 |
--------------------------------------------------------------------------------
/.github/workflows/update-flake.yml:
--------------------------------------------------------------------------------
1 | name: Update Flake Inputs
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | # Every Saturday at 0:00 UTC
6 | - cron: '0 0 * * 6'
7 |
8 | jobs:
9 | update-flake-inputs:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 |
15 | - name: Install Nix
16 | uses: DeterminateSystems/nix-installer-action@main
17 |
18 | - name: Update flake.lock
19 | uses: DeterminateSystems/update-flake-lock@main
20 | with:
21 | token: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Nix/direnv
2 | /result*
3 | /.direnv/
4 |
5 | # Binary destination from `make`
6 | /nixos
7 |
8 | # For bubbletea log files
9 | *.log
10 |
11 | # Built documentation site
12 | /site/
13 |
14 | # Built man pages
15 | /man/
16 |
17 | # Generated documentation artifacts
18 | /doc/src/generated-module.md
19 | /doc/src/generated-settings.md
20 | /doc/man/nixos-cli-settings.5.scd
21 | !/doc/man/nixos-cli-settings.5.scd.template
22 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "singleQuote": true,
6 | "proseWrap": "always"
7 | }
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | APP_NAME := nixos
2 | BUILD_VAR_PKG := github.com/nix-community/nixos-cli/internal/build
3 |
4 | VERSION ?= $(shell git describe --tags --always)
5 | COMMIT_HASH ?= $(shell git rev-parse HEAD)
6 |
7 | # Configurable parameters
8 | FLAKE ?= true
9 | NIXPKGS_REVISION ?= 24.11
10 |
11 | LDFLAGS := -X $(BUILD_VAR_PKG).Version=$(VERSION)
12 | LDFLAGS += -X $(BUILD_VAR_PKG).GitRevision=$(COMMIT_HASH)
13 | LDFLAGS += -X $(BUILD_VAR_PKG).Flake=$(FLAKE)
14 | LDFLAGS += -X $(BUILD_VAR_PKG).NixpkgsVersion=$(NIXPKGS_REVISION)
15 |
16 | # Disable CGO by default. This should be a static executable.
17 | CGO_ENABLED ?= 0
18 |
19 | all: build
20 |
21 | .PHONY: build
22 | build:
23 | @echo "building $(APP_NAME)..."
24 | CGO_ENABLED=$(CGO_ENABLED) go build -o ./$(APP_NAME) -ldflags="$(LDFLAGS)" .
25 |
26 | .PHONY: clean
27 | clean:
28 | @echo "cleaning up..."
29 | go clean
30 | rm -rf site/ man/
31 |
32 | .PHONY: test
33 | test:
34 | @echo "running tests..."
35 | CGO_ENABLED=$(CGO_ENABLED) go test ./...
36 |
37 | .PHONY: gen-docs
38 | gen-docs: gen-manpages gen-site
39 |
40 | .PHONY: site
41 | site: gen-site
42 | # -d is interpreted relative to the book directory.
43 | mdbook build ./doc -d ../site
44 |
45 | .PHONY: gen-site
46 | gen-site:
47 | go run doc/build.go site -r $(COMMIT_HASH)
48 |
49 | .PHONY: gen-site
50 | gen-manpages:
51 | go run doc/build.go man
52 |
53 | .PHONY: serve-site
54 | serve-site:
55 | mdbook serve ./doc --open
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
nixos-cli
2 | A unified NixOS management tool.
3 |
4 | ## Introduction
5 |
6 | Tooling for `nixos` has become quite scattered, and as a result, NixOS can be
7 | pretty obtuse to use. There are many community tools available to fix the
8 | problem, but no all-in-one solution.
9 |
10 | `nixos-cli` is exactly that - an all-in-one tool to manage any NixOS
11 | installation with ease, that consists of:
12 |
13 | - Drop-in replacements for NixOS scripts and tools like `nixos-rebuild`
14 | - Generation manager and option preview TUIs
15 | - Many more
16 |
17 | All available through an easy-to-use (and pretty!) interface.
18 |
19 | High-level documentation is available as a
20 | [website](https://nix-community.github.io/nixos-cli), while a detailed reference
21 | for each command and settings is available in the form of man pages after
22 | installation.
23 |
24 | ## Development
25 |
26 | This application is written in [Go](https://go.dev).
27 |
28 | There are two major directories to keep in mind:
29 |
30 | - `cmd/` :: command structure, contains actual main command implementations
31 | - `internal/` :: anything that is shared between commands, categorized by
32 | functionality
33 |
34 | Each command and subcommand **MUST** go in their own package and match the
35 | command tree that it implements.
36 |
37 | All dependencies for this project are neatly provided in a Nix shell. Run
38 | `nix develop .#` or use [`direnv`](https://direnv.net) to automatically drop
39 | into this Nix shell on changing to this directory.
40 |
41 | In order to build both packages at the same time, run
42 | `nix build .#{nixos,nixosLegacy}`.
43 |
44 | ### Documentation
45 |
46 | Documentation is split into two parts:
47 |
48 | - A documentation website, built using
49 | [`mdbook`](https://rust-lang.github.io/mdBook/)
50 | - Manual pages (`man` pages), generated using
51 | [`scdoc`](https://sr.ht/~sircmpwn/scdoc/)
52 |
53 | They are both managed with a build script at [doc/build.go](./doc/build.go), and
54 | with the following Makefile rules:
55 |
56 | - `make gen-manpages` :: generate `roff`-formatted man pages with `scdoc`
57 | - `make gen-site` :: automatically generate settings/module docs for website
58 | - `make serve-site` :: start a preview server for the `mdbook` website.
59 |
60 | `make gen-site` generates two files:
61 |
62 | - Documentation for all available settings in `config.toml`
63 | - Module documentation for `services.nixos-cli`, built using
64 | [`nix-options-doc`](https://github.com/Thunderbottom/nix-options-doc)
65 |
66 | The rest of the site documentation files are located in [doc/man](./doc/src).
67 |
68 | `make gen-manpages` generates man pages using `scdoc`, and generates one
69 | additional man page file from a template: the available settings for
70 | `nixos-cli-config(5)`.
71 |
72 | Check the build script source for more information on how to work with this.
73 |
74 | ### Versioning
75 |
76 | Version numbers are handled using [semantic versioning](https://semver.org/).
77 | They are also managed using Git tags; every version has a Git tag named with the
78 | version; the tag name does not have a preceding "v".
79 |
80 | Non-released builds have a version number that is suffixed with `"-dev"`. As
81 | such, a tag should always exist on a version number change (which removes the
82 | suffix), and the very next commit will re-introduce the suffix.
83 |
84 | Once a tag is created and pushed, create a GitHub release off this tag.
85 |
86 | The version number is managed inside the Nix derivation at
87 | [package.nix](./package.nix).
88 |
89 | ### CI
90 |
91 | The application must build successfully upon every push to `main`, and this is a
92 | prerequisite for every patch or pull request to be merged.
93 |
94 | Cache artifacts are published in a Cachix cache at https://watersucks.cachix.org
95 | when a release is triggered.
96 |
97 | ## Talk!
98 |
99 | Join the Matrix room at
100 | [#nixos-cli:matrix.org](https://matrix.to/#/#nixos-cli:matrix.org)! It's open
101 | for chatting about NixOS in general, and for making it a better experience for
102 | all that involved.
103 |
104 | I would like for this to become a standard NixOS tool, which means that I want
105 | to cater to potentially many interests. If you would like for any commands to be
106 | implemented that you think fit this project, talk to me on Matrix or file a
107 | GitHub issue.
108 |
--------------------------------------------------------------------------------
/cmd/completion/completion.go:
--------------------------------------------------------------------------------
1 | package completion
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | func CompletionCommand() *cobra.Command {
10 | cmd := cobra.Command{
11 | Use: "completion {bash|zsh|fish}",
12 | Short: "Generate completion scripts",
13 | Long: "Generate completion scripts for use in shells.",
14 | Hidden: true,
15 | DisableFlagsInUseLine: true,
16 | ValidArgs: []string{"bash", "zsh", "fish"},
17 | Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
18 | Run: func(cmd *cobra.Command, args []string) {
19 | switch args[0] {
20 | case "bash":
21 | _ = cmd.Root().GenBashCompletionV2(os.Stdout, true)
22 | case "zsh":
23 | _ = cmd.Root().GenZshCompletion(os.Stdout)
24 | case "fish":
25 | _ = cmd.Root().GenFishCompletion(os.Stdout, true)
26 | }
27 | },
28 | }
29 |
30 | return &cmd
31 | }
32 |
--------------------------------------------------------------------------------
/cmd/features/features.go:
--------------------------------------------------------------------------------
1 | package features
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os/exec"
7 | "runtime"
8 | "strings"
9 |
10 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
11 | "github.com/nix-community/nixos-cli/internal/cmd/utils"
12 | "github.com/nix-community/nixos-cli/internal/logger"
13 | "github.com/spf13/cobra"
14 |
15 | "github.com/nix-community/nixos-cli/internal/build"
16 | )
17 |
18 | func FeatureCommand() *cobra.Command {
19 | opts := cmdOpts.FeaturesOpts{}
20 |
21 | cmd := cobra.Command{
22 | Use: "features",
23 | Short: "Show metadata about this application",
24 | Long: "Show metadata about this application and configured options.",
25 | Run: func(cmd *cobra.Command, args []string) {
26 | featuresMain(cmd, &opts)
27 | },
28 | }
29 |
30 | cmdUtils.SetHelpFlagText(&cmd)
31 |
32 | cmd.Flags().BoolVarP(&opts.DisplayJson, "json", "j", false, "Output information in JSON format")
33 |
34 | return &cmd
35 | }
36 |
37 | type features struct {
38 | Version string `json:"version"`
39 | GitRevision string `json:"git_rev"`
40 | GoVersion string `json:"go_version"`
41 | DetectedNixVersion string `json:"nix_version"`
42 | CompilationOptions complilationOptions `json:"options"`
43 | }
44 |
45 | type complilationOptions struct {
46 | NixpkgsVersion string `json:"nixpkgs_version"`
47 | Flake bool `json:"flake"`
48 | }
49 |
50 | func featuresMain(cmd *cobra.Command, opts *cmdOpts.FeaturesOpts) {
51 | log := logger.FromContext(cmd.Context())
52 |
53 | features := features{
54 | Version: buildOpts.Version,
55 | GitRevision: buildOpts.GitRevision,
56 | GoVersion: runtime.Version(),
57 | CompilationOptions: complilationOptions{
58 | NixpkgsVersion: buildOpts.NixpkgsVersion,
59 | Flake: buildOpts.Flake == "true",
60 | },
61 | }
62 |
63 | nixVersionCmd := exec.Command("nix", "--version")
64 | nixVersionOutput, _ := nixVersionCmd.Output()
65 | if nixVersionCmd.ProcessState.ExitCode() != 0 {
66 | log.Warn("nix version command failed to run, unable to detect nix version")
67 | features.DetectedNixVersion = "unknown"
68 | } else {
69 | features.DetectedNixVersion = strings.Trim(string(nixVersionOutput), "\n ")
70 | }
71 |
72 | if opts.DisplayJson {
73 | bytes, _ := json.MarshalIndent(features, "", " ")
74 | fmt.Printf("%v\n", string(bytes))
75 |
76 | return
77 | }
78 |
79 | fmt.Printf("nixos %v\n", features.Version)
80 | fmt.Printf("git rev: %v\n", features.GitRevision)
81 | fmt.Printf("go version: %v\n", features.GoVersion)
82 | fmt.Printf("nix version: %v\n\n", features.DetectedNixVersion)
83 |
84 | fmt.Println("Compilation Options")
85 | fmt.Println("-------------------")
86 |
87 | fmt.Printf("flake :: %v\n", features.CompilationOptions.Flake)
88 | fmt.Printf("nixpkgs_version :: %v\n", features.CompilationOptions.NixpkgsVersion)
89 | }
90 |
--------------------------------------------------------------------------------
/cmd/generation/delete/resolver.go:
--------------------------------------------------------------------------------
1 | package delete
2 |
3 | import (
4 | "fmt"
5 | "slices"
6 | "sort"
7 | "time"
8 |
9 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
10 | "github.com/nix-community/nixos-cli/internal/generation"
11 | timeUtils "github.com/nix-community/nixos-cli/internal/time"
12 | )
13 |
14 | type generationSet map[uint64]present
15 |
16 | // evil type system hack to avoid typing struct{} all the time
17 | type present struct{}
18 |
19 | func resolveGenerationsToDelete(generations []generation.Generation, opts *cmdOpts.GenerationDeleteOpts) ([]generation.Generation, error) {
20 | currentGenIdx := slices.IndexFunc(generations, func(g generation.Generation) bool {
21 | return g.IsCurrent
22 | })
23 | if currentGenIdx == -1 {
24 | panic("current generation not found, this is a bug")
25 | }
26 | currentGen := generations[currentGenIdx]
27 |
28 | totalGenerations := uint64(len(generations))
29 |
30 | if totalGenerations == 0 {
31 | return nil, fmt.Errorf("no generations exist in profile")
32 | }
33 | if totalGenerations == 1 {
34 | return nil, fmt.Errorf("only one generations exists in profile, cannot delete the current generation")
35 | }
36 |
37 | if opts.MinimumToKeep > 0 && opts.MinimumToKeep >= totalGenerations {
38 | return nil, GenerationResolveMinError{ExpectedMinimum: opts.MinimumToKeep, AvailableGenerations: totalGenerations}
39 | }
40 |
41 | gensToKeep := make(generationSet, len(opts.Keep))
42 | for _, v := range opts.Keep {
43 | gensToKeep[uint64(v)] = present{}
44 | }
45 | gensToKeep[currentGen.Number] = present{}
46 |
47 | gensToRemove := make(generationSet, len(opts.Remove))
48 | for _, v := range opts.Remove {
49 | gensToRemove[uint64(v)] = present{}
50 | }
51 |
52 | if opts.All {
53 | for _, v := range generations {
54 | gensToRemove[v.Number] = present{}
55 | }
56 | } else {
57 | if opts.LowerBound != 0 || opts.UpperBound != 0 {
58 | upperBound := opts.UpperBound
59 | if upperBound == 0 {
60 | upperBound = generations[len(generations)-1].Number
61 | }
62 | lowerBound := opts.LowerBound
63 | if lowerBound == 0 {
64 | lowerBound = generations[0].Number
65 | }
66 |
67 | if lowerBound > upperBound {
68 | return nil, GenerationResolveBoundsError{LowerBound: lowerBound, UpperBound: upperBound}
69 | }
70 | if upperBound > generations[len(generations)-1].Number || upperBound < generations[0].Number {
71 | return nil, GenerationResolveRangeError{InvalidBound: upperBound}
72 | }
73 | if lowerBound < generations[0].Number || lowerBound > generations[len(generations)-1].Number {
74 | return nil, GenerationResolveRangeError{InvalidBound: lowerBound}
75 | }
76 |
77 | for _, v := range generations {
78 | if v.Number >= lowerBound && v.Number <= upperBound {
79 | gensToRemove[v.Number] = present{}
80 | }
81 | }
82 | }
83 |
84 | if opts.OlderThan != "" {
85 | // This is validated during argument parsing, so no need to check for errors.
86 | olderThanTimeSpan, _ := timeUtils.DurationFromTimeSpan(opts.OlderThan)
87 | upperDateBound := time.Now().Add(-olderThanTimeSpan)
88 |
89 | for _, v := range generations {
90 | if v.CreationDate.Before(upperDateBound) {
91 | gensToRemove[v.Number] = present{}
92 | }
93 | }
94 | }
95 | }
96 |
97 | for g := range gensToKeep {
98 | delete(gensToRemove, g)
99 | }
100 |
101 | remainingGenCount := uint64(len(generations) - len(gensToRemove))
102 | if opts.MinimumToKeep > 0 && remainingGenCount < opts.MinimumToKeep {
103 | for j := range generations {
104 | i := len(generations) - 1 - j
105 | g := generations[i]
106 |
107 | delete(gensToRemove, g.Number)
108 |
109 | remainingGenCount = uint64(len(generations) - len(gensToRemove))
110 | if remainingGenCount == opts.MinimumToKeep {
111 | break
112 | }
113 | }
114 | }
115 |
116 | if len(gensToRemove) == 0 {
117 | return nil, GenerationResolveNoneFoundError{}
118 | }
119 |
120 | result := make([]generation.Generation, 0, len(gensToRemove))
121 | for num := range gensToRemove {
122 | for _, g := range generations {
123 | if g.Number == num {
124 | result = append(result, g)
125 | }
126 | }
127 | }
128 |
129 | sort.Slice(result, func(i, j int) bool {
130 | return result[i].Number < result[j].Number
131 | })
132 | return result, nil
133 | }
134 |
135 | type GenerationResolveMinError struct {
136 | ExpectedMinimum uint64
137 | AvailableGenerations uint64
138 | }
139 |
140 | func (e GenerationResolveMinError) Error() string {
141 | return fmt.Sprintf("cannot keep %v generations, there are only %v available", e.ExpectedMinimum, e.AvailableGenerations)
142 | }
143 |
144 | type GenerationResolveBoundsError struct {
145 | LowerBound uint64
146 | UpperBound uint64
147 | }
148 |
149 | func (e GenerationResolveBoundsError) Error() string {
150 | return fmt.Sprintf("lower bound '%v' must be less than upper bound '%v'", e.LowerBound, e.UpperBound)
151 | }
152 |
153 | type GenerationResolveRangeError struct {
154 | InvalidBound uint64
155 | }
156 |
157 | func (e GenerationResolveRangeError) Error() string {
158 | return fmt.Sprintf("bound '%v' is not within the range of available generations", e.InvalidBound)
159 | }
160 |
161 | type GenerationResolveNoneFoundError struct{}
162 |
163 | func (e GenerationResolveNoneFoundError) Error() string {
164 | return "no generations were resolved for deletion from the given parameters"
165 | }
166 |
--------------------------------------------------------------------------------
/cmd/generation/delete/resolver_test.go:
--------------------------------------------------------------------------------
1 | package delete
2 |
3 | import (
4 | "errors"
5 | "reflect"
6 | "testing"
7 | "time"
8 |
9 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
10 | "github.com/nix-community/nixos-cli/internal/generation"
11 | )
12 |
13 | func TestResolveGenerationsToDelete(t *testing.T) {
14 | timeNow := time.Now()
15 | generations := []generation.Generation{
16 | {Number: 1, CreationDate: timeNow.Add(-48 * time.Hour), IsCurrent: false},
17 | {Number: 2, CreationDate: timeNow.Add(-24 * time.Hour), IsCurrent: false},
18 | {Number: 3, CreationDate: timeNow, IsCurrent: true},
19 | }
20 |
21 | tests := []struct {
22 | name string
23 | opts *cmdOpts.GenerationDeleteOpts
24 | expect []uint64
25 | expectErr error
26 | }{
27 | {
28 | name: "Delete all generations",
29 | opts: &cmdOpts.GenerationDeleteOpts{
30 | All: true,
31 | },
32 | expect: []uint64{1, 2},
33 | },
34 | {
35 | name: "Keep specific generations",
36 | opts: &cmdOpts.GenerationDeleteOpts{
37 | Keep: []uint{1},
38 | All: true,
39 | },
40 | expect: []uint64{2},
41 | },
42 | {
43 | name: "Minimum to keep",
44 | opts: &cmdOpts.GenerationDeleteOpts{
45 | MinimumToKeep: 3,
46 | },
47 | expect: []uint64{},
48 | expectErr: GenerationResolveMinError{
49 | ExpectedMinimum: 3,
50 | AvailableGenerations: 3,
51 | },
52 | },
53 | {
54 | name: "Lower and upper bounds",
55 | opts: &cmdOpts.GenerationDeleteOpts{
56 | LowerBound: 1,
57 | UpperBound: 2,
58 | },
59 | expect: []uint64{1, 2},
60 | },
61 | {
62 | name: "Older than specified duration",
63 | opts: &cmdOpts.GenerationDeleteOpts{
64 | OlderThan: "24h",
65 | },
66 | expect: []uint64{1, 2},
67 | },
68 | {
69 | name: "Remove specific generations",
70 | opts: &cmdOpts.GenerationDeleteOpts{
71 | Remove: []uint{1},
72 | },
73 | expect: []uint64{1},
74 | },
75 | {
76 | name: "Invalid lower and upper bounds",
77 | opts: &cmdOpts.GenerationDeleteOpts{
78 | LowerBound: 3,
79 | UpperBound: 1,
80 | },
81 | expectErr: GenerationResolveBoundsError{LowerBound: 3, UpperBound: 1},
82 | },
83 | }
84 |
85 | for _, test := range tests {
86 | t.Run(test.name, func(t *testing.T) {
87 | result, err := resolveGenerationsToDelete(generations, test.opts)
88 |
89 | if test.expectErr != nil {
90 | if !errors.Is(err, test.expectErr) {
91 | t.Errorf("expected error %v, got %v", test.expectErr, err)
92 | }
93 | } else {
94 | if err != nil {
95 | t.Errorf("unexpected error: %v", err)
96 | }
97 |
98 | resultNumbers := make([]uint64, len(result))
99 | for i, g := range result {
100 | resultNumbers[i] = g.Number
101 | }
102 |
103 | if !reflect.DeepEqual(test.expect, resultNumbers) {
104 | t.Errorf("expected %v, got %v", test.expect, resultNumbers)
105 | }
106 | }
107 | })
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/cmd/generation/diff/diff.go:
--------------------------------------------------------------------------------
1 | package diff
2 |
3 | import (
4 | "fmt"
5 | "path/filepath"
6 | "strconv"
7 |
8 | "github.com/spf13/cobra"
9 |
10 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
11 | "github.com/nix-community/nixos-cli/internal/cmd/utils"
12 | "github.com/nix-community/nixos-cli/internal/constants"
13 | "github.com/nix-community/nixos-cli/internal/generation"
14 | "github.com/nix-community/nixos-cli/internal/logger"
15 | "github.com/nix-community/nixos-cli/internal/settings"
16 | "github.com/nix-community/nixos-cli/internal/system"
17 | )
18 |
19 | func GenerationDiffCommand(genOpts *cmdOpts.GenerationOpts) *cobra.Command {
20 | opts := cmdOpts.GenerationDiffOpts{}
21 |
22 | cmd := cobra.Command{
23 | Use: "diff {BEFORE} {AFTER}",
24 | Short: "Show what changed between two generations",
25 | Long: "Display what paths differ between two generations.",
26 | Args: func(cmd *cobra.Command, args []string) error {
27 | if err := cobra.ExactArgs(2)(cmd, args); err != nil {
28 | return err
29 | }
30 |
31 | before, err := strconv.ParseInt(args[0], 10, 32)
32 | if err != nil {
33 | return fmt.Errorf("{BEFORE} must be an integer, got '%v'", before)
34 | }
35 | opts.Before = uint(before)
36 |
37 | after, err := strconv.ParseInt(args[1], 10, 32)
38 | if err != nil {
39 | return fmt.Errorf("{AFTER} must be an integer, got '%v'", after)
40 | }
41 | opts.After = uint(after)
42 |
43 | return nil
44 | },
45 | ValidArgsFunction: generation.CompleteGenerationNumber(&genOpts.ProfileName, 2),
46 | RunE: func(cmd *cobra.Command, args []string) error {
47 | return cmdUtils.CommandErrorHandler(generationDiffMain(cmd, genOpts, &opts))
48 | },
49 | }
50 |
51 | cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose logging")
52 |
53 | cmd.SetHelpTemplate(cmd.HelpTemplate() + `
54 | Arguments:
55 | [BEFORE] Number of first generation to compare with
56 | [AFTER] Number of second generation to compare with
57 | `)
58 | cmdUtils.SetHelpFlagText(&cmd)
59 |
60 | return &cmd
61 | }
62 |
63 | func generationDiffMain(cmd *cobra.Command, genOpts *cmdOpts.GenerationOpts, opts *cmdOpts.GenerationDiffOpts) error {
64 | log := logger.FromContext(cmd.Context())
65 | cfg := settings.FromContext(cmd.Context())
66 | s := system.NewLocalSystem(log)
67 |
68 | profileDirectory := constants.NixProfileDirectory
69 | if genOpts.ProfileName != "system" {
70 | profileDirectory = constants.NixSystemProfileDirectory
71 | }
72 |
73 | beforeDirectory := filepath.Join(profileDirectory, fmt.Sprintf("%v-%v-link", genOpts.ProfileName, opts.Before))
74 | afterDirectory := filepath.Join(profileDirectory, fmt.Sprintf("%v-%v-link", genOpts.ProfileName, opts.After))
75 |
76 | err := generation.RunDiffCommand(log, s, beforeDirectory, afterDirectory, &generation.DiffCommandOptions{
77 | UseNvd: cfg.UseNvd,
78 | Verbose: opts.Verbose,
79 | })
80 | if err != nil {
81 | log.Errorf("failed to run diff command: %v", err)
82 | return err
83 | }
84 |
85 | return nil
86 | }
87 |
--------------------------------------------------------------------------------
/cmd/generation/generation.go:
--------------------------------------------------------------------------------
1 | package generation
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 |
6 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
7 | "github.com/nix-community/nixos-cli/internal/cmd/utils"
8 | "github.com/nix-community/nixos-cli/internal/generation"
9 |
10 | genDeleteCmd "github.com/nix-community/nixos-cli/cmd/generation/delete"
11 | genDiffCmd "github.com/nix-community/nixos-cli/cmd/generation/diff"
12 | genListCmd "github.com/nix-community/nixos-cli/cmd/generation/list"
13 | genRollbackCmd "github.com/nix-community/nixos-cli/cmd/generation/rollback"
14 | genSwitchCmd "github.com/nix-community/nixos-cli/cmd/generation/switch"
15 | )
16 |
17 | func GenerationCommand() *cobra.Command {
18 | opts := cmdOpts.GenerationOpts{}
19 |
20 | cmd := cobra.Command{
21 | Use: "generation {command}",
22 | Short: "Manage NixOS generations",
23 | Long: "Manage NixOS generations on this machine.",
24 | }
25 |
26 | cmd.PersistentFlags().StringVarP(&opts.ProfileName, "profile", "p", "system", "System profile to use")
27 |
28 | cmd.AddCommand(genDeleteCmd.GenerationDeleteCommand(&opts))
29 | cmd.AddCommand(genDiffCmd.GenerationDiffCommand(&opts))
30 | cmd.AddCommand(genListCmd.GenerationListCommand(&opts))
31 | cmd.AddCommand(genSwitchCmd.GenerationSwitchCommand(&opts))
32 | cmd.AddCommand(genRollbackCmd.GenerationRollbackCommand(&opts))
33 |
34 | cmdUtils.SetHelpFlagText(&cmd)
35 |
36 | _ = cmd.RegisterFlagCompletionFunc("profile", generation.CompleteProfileFlag)
37 |
38 | return &cmd
39 | }
40 |
--------------------------------------------------------------------------------
/cmd/generation/list/list.go:
--------------------------------------------------------------------------------
1 | package list
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "strings"
8 | "time"
9 |
10 | "github.com/spf13/cobra"
11 |
12 | "github.com/nix-community/nixos-cli/cmd/generation/shared"
13 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
14 | "github.com/nix-community/nixos-cli/internal/cmd/utils"
15 | "github.com/nix-community/nixos-cli/internal/generation"
16 | "github.com/nix-community/nixos-cli/internal/logger"
17 | "github.com/olekukonko/tablewriter"
18 | )
19 |
20 | func GenerationListCommand(genOpts *cmdOpts.GenerationOpts) *cobra.Command {
21 | opts := cmdOpts.GenerationListOpts{}
22 |
23 | cmd := cobra.Command{
24 | Use: "list",
25 | Short: "List all NixOS generations in a profile",
26 | Long: "List all generations in a NixOS profile and their details.",
27 | RunE: func(cmd *cobra.Command, args []string) error {
28 | return cmdUtils.CommandErrorHandler(generationListMain(cmd, genOpts, &opts))
29 | },
30 | }
31 |
32 | cmd.Flags().BoolVarP(&opts.DisplayJson, "json", "j", false, "Display in JSON format")
33 | cmd.Flags().BoolVarP(&opts.DisplayTable, "table", "t", false, "Display in table format")
34 |
35 | cmdUtils.SetHelpFlagText(&cmd)
36 |
37 | return &cmd
38 | }
39 |
40 | func generationListMain(cmd *cobra.Command, genOpts *cmdOpts.GenerationOpts, opts *cmdOpts.GenerationListOpts) error {
41 | log := logger.FromContext(cmd.Context())
42 |
43 | generations, err := genUtils.LoadGenerations(log, genOpts.ProfileName, true)
44 | if err != nil {
45 | return err
46 | }
47 |
48 | if opts.DisplayTable {
49 | displayTable(generations)
50 | return nil
51 | }
52 |
53 | if opts.DisplayJson {
54 | bytes, _ := json.MarshalIndent(generations, "", " ")
55 | fmt.Printf("%v\n", string(bytes))
56 |
57 | return nil
58 | }
59 |
60 | err = generationUI(log, genOpts.ProfileName, generations)
61 | if err != nil {
62 | log.Errorf("error running generation TUI: %v", err)
63 | return err
64 | }
65 |
66 | return nil
67 | }
68 |
69 | func displayTable(generations []generation.Generation) {
70 | data := make([][]string, len(generations))
71 |
72 | for i, v := range generations {
73 | data[i] = []string{
74 | fmt.Sprintf("%v", v.Number),
75 | fmt.Sprintf("%v", v.IsCurrent),
76 | fmt.Sprintf("%v", v.CreationDate.Format(time.ANSIC)),
77 | v.NixosVersion,
78 | v.NixpkgsRevision,
79 | v.ConfigurationRevision,
80 | v.KernelVersion,
81 | strings.Join(v.Specialisations, ","),
82 | }
83 | }
84 |
85 | table := tablewriter.NewWriter(os.Stdout)
86 | table.SetHeader([]string{"Number", "Current", "Date", "NixOS Version", "Nixpkgs Version", "Config Version", "Kernel Version", "Specialisations"})
87 | table.SetAutoWrapText(false)
88 | table.SetAutoFormatHeaders(true)
89 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
90 | table.SetAlignment(tablewriter.ALIGN_LEFT)
91 | table.SetCenterSeparator("")
92 | table.SetColumnSeparator("")
93 | table.SetRowSeparator("")
94 | table.SetHeaderLine(false)
95 | table.SetBorder(false)
96 | table.SetTablePadding("\t")
97 | table.SetNoWhiteSpace(true)
98 | table.AppendBulk(data)
99 | table.Render()
100 | }
101 |
--------------------------------------------------------------------------------
/cmd/generation/shared/utils.go:
--------------------------------------------------------------------------------
1 | package genUtils
2 |
3 | import (
4 | "github.com/nix-community/nixos-cli/internal/generation"
5 | "github.com/nix-community/nixos-cli/internal/logger"
6 | )
7 |
8 | func LoadGenerations(log *logger.Logger, profileName string, reverse bool) ([]generation.Generation, error) {
9 | generations, err := generation.CollectGenerationsInProfile(log, profileName)
10 | if err != nil {
11 | switch v := err.(type) {
12 | case *generation.GenerationReadError:
13 | for _, err := range v.Errors {
14 | log.Warnf("%v", err)
15 | }
16 |
17 | default:
18 | log.Errorf("error collecting generation information: %v", v)
19 | return nil, v
20 | }
21 | }
22 |
23 | if reverse {
24 | for i, j := 0, len(generations)-1; i < j; i, j = i+1, j-1 {
25 | generations[i], generations[j] = generations[j], generations[i]
26 | }
27 | }
28 |
29 | return generations, nil
30 | }
31 |
--------------------------------------------------------------------------------
/cmd/info/info.go:
--------------------------------------------------------------------------------
1 | package info
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/fatih/color"
9 | "github.com/nix-community/nixos-cli/internal/activation"
10 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
11 | "github.com/nix-community/nixos-cli/internal/cmd/utils"
12 | "github.com/nix-community/nixos-cli/internal/constants"
13 | "github.com/nix-community/nixos-cli/internal/generation"
14 | "github.com/nix-community/nixos-cli/internal/logger"
15 | "github.com/spf13/cobra"
16 | )
17 |
18 | func InfoCommand() *cobra.Command {
19 | opts := cmdOpts.InfoOpts{}
20 |
21 | cmd := cobra.Command{
22 | Use: "info",
23 | Short: "Show info about the currently running generation",
24 | Long: "Show information about the currently running NixOS generation.",
25 | RunE: func(cmd *cobra.Command, args []string) error {
26 | return cmdUtils.CommandErrorHandler(infoMain(cmd, &opts))
27 | },
28 | }
29 |
30 | cmdUtils.SetHelpFlagText(&cmd)
31 |
32 | cmd.Flags().BoolVarP(&opts.DisplayJson, "json", "j", false, "Format output as JSON")
33 | cmd.Flags().BoolVarP(&opts.DisplayMarkdown, "markdown", "m", false, "Format output as Markdown for reporting")
34 |
35 | return &cmd
36 | }
37 |
38 | const (
39 | markdownTemplate = `- nixos version: %v
40 | - nixpkgs revision: %v
41 | - kernel version: %v
42 | `
43 | )
44 |
45 | func infoMain(cmd *cobra.Command, opts *cmdOpts.InfoOpts) error {
46 | log := logger.FromContext(cmd.Context())
47 |
48 | // Only support the `system` profile for now.
49 | currentGenNumber, err := activation.GetCurrentGenerationNumber("system")
50 | if err != nil {
51 | log.Warnf("failed to determine current generation number: %v", err)
52 | return err
53 | }
54 |
55 | currentGen, err := generation.GenerationFromDirectory(constants.CurrentSystem, currentGenNumber)
56 | if err != nil {
57 | log.Warnf("failed to collect generations: %v", err)
58 | return err
59 | }
60 | currentGen.Number = currentGenNumber
61 | currentGen.IsCurrent = true
62 |
63 | if opts.DisplayJson {
64 | bytes, _ := json.MarshalIndent(currentGen, "", " ")
65 | fmt.Printf("%v\n", string(bytes))
66 | return nil
67 | }
68 |
69 | if opts.DisplayMarkdown {
70 | fmt.Printf(markdownTemplate, currentGen.NixosVersion, currentGen.NixpkgsRevision, currentGen.KernelVersion)
71 | return nil
72 | }
73 |
74 | prettyPrintGenInfo(currentGen)
75 |
76 | return nil
77 | }
78 |
79 | var titleColor = color.New(color.Bold, color.Italic)
80 |
81 | func prettyPrintGenInfo(g *generation.Generation) {
82 | version := g.NixosVersion
83 | if version == "" {
84 | version = "NixOS (unknown version)"
85 | }
86 |
87 | titleColor.Printf("%v\n", version)
88 | titleColor.Println(strings.Repeat("-", len(version)))
89 |
90 | printKey("Generation")
91 | fmt.Println(g.Number)
92 |
93 | printKey("Description")
94 | desc := g.Description
95 | if desc == "" {
96 | desc = color.New(color.Italic).Sprint("(none)")
97 | }
98 | fmt.Println(desc)
99 |
100 | printKey("Nixpkgs Version")
101 | nixpkgsVersion := g.NixpkgsRevision
102 | if nixpkgsVersion == "" {
103 | nixpkgsVersion = color.New(color.Italic).Sprint("(unknown)")
104 | }
105 | fmt.Println(nixpkgsVersion)
106 |
107 | printKey("Config Version")
108 | configVersion := g.ConfigurationRevision
109 | if configVersion == "" {
110 | configVersion = color.New(color.Italic).Sprint("(unknown)")
111 | }
112 | fmt.Println(configVersion)
113 |
114 | printKey("Kernel Version")
115 | kernelVersion := g.KernelVersion
116 | if kernelVersion == "" {
117 | kernelVersion = color.New(color.Italic).Sprint("(unknown)")
118 | }
119 | fmt.Println(kernelVersion)
120 |
121 | printKey("Specialisations")
122 | specialisations := strings.Join(g.Specialisations, ", ")
123 | if specialisations == "" {
124 | specialisations = color.New(color.Italic).Sprint("(none)")
125 | }
126 | fmt.Println(specialisations)
127 | }
128 |
129 | func getKeyMaxLength() int {
130 | strings := []string{
131 | "Generation", "Description", "NixOS Version", "Nixpkgs Version",
132 | "Config Version", "Kernel Version", "Specialisations",
133 | }
134 |
135 | maxLength := 0
136 |
137 | for _, v := range strings {
138 | l := len(color.CyanString(v))
139 | if l > maxLength {
140 | maxLength = l
141 | }
142 | }
143 |
144 | return maxLength
145 | }
146 |
147 | func printKey(key string) {
148 | fmt.Printf("%-"+fmt.Sprintf("%v", keyMaxLength)+"v :: ", color.CyanString(key))
149 | }
150 |
151 | var keyMaxLength = getKeyMaxLength()
152 |
--------------------------------------------------------------------------------
/cmd/init/configuration.nix.txt:
--------------------------------------------------------------------------------
1 | # Edit this configuration file to define what should be installed on
2 | # your system. Help is available in the configuration.nix(5) man page, on
3 | # https://search.nixos.org/options and in the NixOS manual (`nixos-help`).
4 |
5 | {
6 | config,
7 | lib,
8 | pkgs,
9 | ...
10 | }: {
11 | imports = [
12 | # Include the results of the hardware scan.
13 | ./hardware-configuration.nix
14 | ];
15 |
16 | # Bootloader config
17 | %s
18 | # networking.hostName = "nixos"; # Define your hostname.
19 | # Pick only one of the below networking options.
20 | # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
21 | # networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
22 |
23 | # Set your time zone.
24 | # time.timeZone = "Europe/Amsterdam";
25 |
26 | # Configure network proxy if necessary
27 | # networking.proxy.default = "http://user:password\@proxy:port/";
28 | # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
29 |
30 | # Select internationalisation properties.
31 | # i18n.defaultLocale = "en_US.UTF-8";
32 | # console = {
33 | # font = "Lat2-Terminus16";
34 | # keyMap = "us";
35 | # useXkbConfig = true; # use xkb.options in tty.
36 | # };
37 |
38 | %s
39 | # Configure keymap in X11
40 | # services.xserver.xkb.layout = "us";
41 | # services.xserver.xkb.options = "eurosign:e,caps:escape";
42 |
43 | # Desktop configuration
44 | %s
45 | # Enable CUPS to print documents.
46 | # services.printing.enable = true;
47 |
48 | # Enable sound.
49 | # sound.enable = true;
50 | # hardware.pulseaudio.enable = true;
51 |
52 | # Enable touchpad support (enabled default in most desktop managers).
53 | # services.xserver.libinput.enable = true;
54 |
55 | # Define a user account. Don't forget to set a password with `passwd`.
56 | # Or, if you don't want users to set a password imperatively, set
57 | # `users.mutableUsers` to false and specify password using the
58 | # `users.passwordFile` or `users.hashedPassword` options.
59 | # users.users.alice = {
60 | # isNormalUser = true;
61 | # extraGroups = ["wheel"]; # Enable ‘sudo’ for the user.
62 | # packages = with pkgs; [
63 | # firefox
64 | # tree
65 | # ];
66 | # };
67 |
68 | # List packages installed in system profile. To search, run:
69 | # $ nix search wget
70 | # environment.systemPackages = with pkgs; [
71 | # vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
72 | # wget
73 | # ];
74 |
75 | # Some programs need SUID wrappers, can be configured further or are
76 | # started in user sessions.
77 | # programs.mtr.enable = true;
78 | # programs.gnupg.agent = {
79 | # enable = true;
80 | # enableSSHSupport = true;
81 | # };
82 |
83 | # List services that you want to enable:
84 |
85 | # Enable the OpenSSH daemon.
86 | # services.openssh.enable = true;
87 |
88 | # Open ports in the firewall.
89 | # networking.firewall.allowedTCPPorts = [ ... ];
90 | # networking.firewall.allowedUDPPorts = [ ... ];
91 | # Or disable the firewall altogether.
92 | # networking.firewall.enable = false;
93 | %s
94 | # Copy the NixOS configuration file and link it from the resulting system
95 | # (/run/current-system/configuration.nix). This is useful in case you
96 | # accidentally delete configuration.nix.
97 | # system.copySystemConfiguration = true;
98 |
99 | # This option defines the first version of NixOS you have installed on this particular machine,
100 | # and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions.
101 | #
102 | # Most users should NEVER change this value after the initial install, for any reason,
103 | # even if you've upgraded your system to a new NixOS release.
104 | #
105 | # This value does NOT affect the Nixpkgs version your packages and OS are pulled from,
106 | # so changing it will NOT upgrade your system.
107 | #
108 | # This value being lower than the current NixOS release does NOT mean your system is
109 | # out of date, out of support, or vulnerable.
110 | #
111 | # Do NOT change this value unless you have manually inspected all the changes it would make to your configuration,
112 | # and migrated your data accordingly.
113 | #
114 | # For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
115 | system.stateVersion = "%s"; # Did you read the comment?
116 | }
117 |
--------------------------------------------------------------------------------
/cmd/init/cpuinfo.go:
--------------------------------------------------------------------------------
1 | package init
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "os"
7 | "strings"
8 |
9 | "github.com/nix-community/nixos-cli/internal/logger"
10 | "github.com/nix-community/nixos-cli/internal/system"
11 | )
12 |
13 | type CPUInfo struct {
14 | VirtualisationEnabled bool
15 | Manufacturer CPUManufacturer
16 | }
17 |
18 | type CPUManufacturer int
19 |
20 | const (
21 | manufacturerIntel CPUManufacturer = iota
22 | manufacturerAMD
23 | manufacturerUnknown
24 | )
25 |
26 | func (c CPUManufacturer) CPUType() string {
27 | switch c {
28 | case manufacturerIntel:
29 | return "Intel"
30 | case manufacturerAMD:
31 | return "AMD"
32 | default:
33 | return "unknown"
34 | }
35 | }
36 |
37 | func getCPUInfo(log *logger.Logger) *CPUInfo {
38 | result := &CPUInfo{
39 | VirtualisationEnabled: false,
40 | Manufacturer: manufacturerUnknown,
41 | }
42 |
43 | cpuinfoFile, err := os.Open("/proc/cpuinfo")
44 | if err != nil {
45 | log.Warnf("failed to open /proc/cpuinfo: %v", err)
46 | return result
47 | }
48 |
49 | defer cpuinfoFile.Close()
50 |
51 | s := bufio.NewScanner(cpuinfoFile)
52 | s.Split(bufio.ScanLines)
53 |
54 | for s.Scan() {
55 | line := s.Text()
56 | if strings.HasPrefix(line, "flags") {
57 | if strings.Contains(line, "vmx") || strings.Contains(line, "svm") {
58 | result.VirtualisationEnabled = true
59 | }
60 | } else if strings.HasPrefix(line, "vendor_id") {
61 | if strings.Contains(line, "GenuineIntel") {
62 | result.Manufacturer = manufacturerIntel
63 | } else if strings.Contains(line, "AuthenticAMD") {
64 | result.Manufacturer = manufacturerAMD
65 | }
66 | }
67 | }
68 |
69 | return result
70 | }
71 |
72 | type VirtualisationType int
73 |
74 | const (
75 | VirtualisationTypeNone VirtualisationType = iota
76 | VirtualisationTypeOracle
77 | VirtualisationTypeParallels
78 | VirtualisationTypeQemu
79 | VirtualisationTypeKVM
80 | VirtualisationTypeBochs
81 | VirtualisationTypeHyperV
82 | VirtualisationTypeSystemdNspawn
83 | VirtualisationTypeUnknown
84 | )
85 |
86 | func (v VirtualisationType) String() string {
87 | switch v {
88 | case VirtualisationTypeOracle:
89 | return "Oracle"
90 | case VirtualisationTypeParallels:
91 | return "Parallels"
92 | case VirtualisationTypeQemu:
93 | return "QEMU"
94 | case VirtualisationTypeKVM:
95 | return "KVM"
96 | case VirtualisationTypeBochs:
97 | return "Bochs"
98 | case VirtualisationTypeHyperV:
99 | return "Hyper-V"
100 | case VirtualisationTypeSystemdNspawn:
101 | return "systemd-nspawn"
102 | case VirtualisationTypeNone:
103 | return "none"
104 | default:
105 | return "unknown"
106 | }
107 | }
108 |
109 | func determineVirtualisationType(s system.CommandRunner, log *logger.Logger) VirtualisationType {
110 | cmd := system.NewCommand("systemd-detect-virt")
111 |
112 | var stdout bytes.Buffer
113 | cmd.Stdout = &stdout
114 |
115 | _, err := s.Run(cmd)
116 | virtType := strings.TrimSpace(stdout.String())
117 |
118 | if err != nil {
119 | // Because yes, this fails with exit status 1. Stupid.
120 | if virtType == "none" {
121 | return VirtualisationTypeNone
122 | }
123 |
124 | log.Warnf("failed to run systemd-detect-virt: %v", err)
125 | return VirtualisationTypeUnknown
126 | }
127 |
128 | switch virtType {
129 | case "oracle":
130 | return VirtualisationTypeOracle
131 | case "parallels":
132 | return VirtualisationTypeParallels
133 | case "qemu":
134 | return VirtualisationTypeQemu
135 | case "kvm":
136 | return VirtualisationTypeKVM
137 | case "bochs":
138 | return VirtualisationTypeBochs
139 | case "microsoft":
140 | return VirtualisationTypeHyperV
141 | case "systemd-nspawn":
142 | return VirtualisationTypeSystemdNspawn
143 | default:
144 | log.Warnf("unknown virtualisation type: %v", virtType)
145 | return VirtualisationTypeUnknown
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/cmd/init/devices.go:
--------------------------------------------------------------------------------
1 | package init
2 |
3 | import (
4 | "net"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 |
9 | "github.com/nix-community/nixos-cli/internal/logger"
10 | )
11 |
12 | const (
13 | pciDeviceDirname = "/sys/bus/pci/devices"
14 | usbDeviceDirname = "/sys/bus/usb/devices"
15 | blockDeviceDirname = "/sys/class/block"
16 | mmcDeviceDirname = "/sys/class/mmc_host"
17 | networkDeviceDirname = "/sys/class/net"
18 | )
19 |
20 | var (
21 | // Device numbers that use the Broadcom STA driver (wl.ko)
22 | broadcomStaDevices = []string{
23 | "0x4311", "0x4312", "0x4313", "0x4315",
24 | "0x4327", "0x4328", "0x4329", "0x432a",
25 | "0x432b", "0x432c", "0x432d", "0x4353",
26 | "0x4357", "0x4358", "0x4359", "0x4331",
27 | "0x43a0", "0x43b1",
28 | }
29 | //
30 | broadcomFullmacDevices = []string{
31 | "0x43a3", "0x43df", "0x43ec", "0x43d3",
32 | "0x43d9", "0x43e9", "0x43ba", "0x43bb",
33 | "0x43bc", "0xaa52", "0x43ca", "0x43cb",
34 | "0x43cc", "0x43c3", "0x43c4", "0x43c5",
35 | }
36 | virtioScsiDevices = []string{"0x1004", "0x1048"}
37 | intel2200bgDevices = []string{
38 | "0x1043", "0x104f", "0x4220",
39 | "0x4221", "0x4223", "0x4224",
40 | }
41 | intel3945abgDevices = []string{
42 | "0x4229", "0x4230", "0x4222", "0x4227",
43 | }
44 | )
45 |
46 | func findPCIDevices(h *hardwareConfigSettings, log *logger.Logger) {
47 | entries, err := os.ReadDir(pciDeviceDirname)
48 | if err != nil {
49 | log.Warnf("failed to read %v: %v", pciDeviceDirname, err)
50 | return
51 | }
52 |
53 | findDevices:
54 | for _, entry := range entries {
55 | devicePath := filepath.Join(pciDeviceDirname, entry.Name())
56 |
57 | vendorFilename := filepath.Join(devicePath, "vendor")
58 | deviceFilename := filepath.Join(devicePath, "device")
59 | classFilename := filepath.Join(devicePath, "class")
60 |
61 | vendorContents, _ := os.ReadFile(vendorFilename)
62 | deviceContents, _ := os.ReadFile(deviceFilename)
63 | classContents, _ := os.ReadFile(classFilename)
64 |
65 | vendor := strings.TrimSpace(string(vendorContents))
66 | device := strings.TrimSpace(string(deviceContents))
67 | class := strings.TrimSpace(string(classContents))
68 |
69 | requiredModuleName := findModuleName(devicePath)
70 | if requiredModuleName != "" {
71 | // Add mass storage controllers, Firewire controllers, or USB controllers
72 | // (respectively) to the initrd modules list.
73 | if strings.HasPrefix(class, "0x01") || strings.HasPrefix(class, "0x02") || strings.HasPrefix(class, "0x0c03") {
74 | *h.InitrdAvailableModules = append(*h.InitrdAvailableModules, requiredModuleName)
75 | }
76 | }
77 |
78 | if vendor == "0x14e4" {
79 | // Broadcom devices
80 | for _, d := range broadcomStaDevices {
81 | if d == device {
82 | *h.ModulePackages = append(*h.ModulePackages, "config.boot.kernelPackages.broadcom_sta")
83 | *h.KernelModules = append(*h.KernelModules, "wl")
84 | continue findDevices
85 | }
86 | }
87 |
88 | for _, d := range broadcomFullmacDevices {
89 | if d == device {
90 | *h.ModulePackages = append(*h.ModulePackages, `(modulesPath + "/hardware/network/broadcom-43xx.nix")`)
91 | continue findDevices
92 | }
93 | }
94 | } else if vendor == "0x1af4" {
95 | // VirtIO SCSI devices
96 | for _, d := range virtioScsiDevices {
97 | if d == device {
98 | *h.InitrdAvailableModules = append(*h.InitrdAvailableModules, "virtio_scsi")
99 | continue findDevices
100 | }
101 | }
102 | } else if vendor == "0x8086" {
103 | // Intel devices
104 | for _, d := range intel2200bgDevices {
105 | if d == device {
106 | *h.Attrs = append(*h.Attrs, KVPair{Key: "networking.enableIntel2200BGFirmware", Value: "true"})
107 | continue findDevices
108 | }
109 | }
110 |
111 | for _, d := range intel3945abgDevices {
112 | if d == device {
113 | *h.Attrs = append(*h.Attrs, KVPair{Key: "networking.enableIntel3945ABGFirmware", Value: "true"})
114 | continue findDevices
115 | }
116 | }
117 | }
118 | }
119 | }
120 |
121 | func findUSBDevices(h *hardwareConfigSettings, log *logger.Logger) {
122 | entries, err := os.ReadDir(usbDeviceDirname)
123 | if err != nil {
124 | log.Warnf("failed to read %s: %v", usbDeviceDirname, err)
125 | return
126 | }
127 |
128 | for _, entry := range entries {
129 | devicePath := filepath.Join(usbDeviceDirname, entry.Name())
130 |
131 | classFilename := filepath.Join(devicePath, "bInterfaceClass")
132 | protocolFilename := filepath.Join(devicePath, "bInterfaceProtocol")
133 |
134 | classContents, _ := os.ReadFile(classFilename)
135 | protocolContents, _ := os.ReadFile(protocolFilename)
136 |
137 | class := strings.TrimSpace(string(classContents))
138 | protocol := strings.TrimSpace(string(protocolContents))
139 |
140 | moduleName := findModuleName(devicePath)
141 |
142 | // Add modules for USB mass storage controllers (first condition) or keyboards (second condition)
143 | if strings.HasPrefix(class, "08") || (strings.HasPrefix(class, "03") && strings.HasPrefix(protocol, "01")) {
144 | *h.InitrdAvailableModules = append(*h.InitrdAvailableModules, moduleName)
145 | }
146 | }
147 | }
148 |
149 | func findGenericDevicesInDir(h *hardwareConfigSettings, log *logger.Logger, deviceDirname string) {
150 | entries, err := os.ReadDir(deviceDirname)
151 | if err != nil {
152 | log.Warnf("failed to read %v: %v", deviceDirname, err)
153 | return
154 | }
155 |
156 | for _, entry := range entries {
157 | devicePath := filepath.Join(blockDeviceDirname, entry.Name(), "device")
158 |
159 | moduleName := findModuleName(devicePath)
160 | if moduleName != "" {
161 | *h.InitrdAvailableModules = append(*h.InitrdAvailableModules, moduleName)
162 | }
163 | }
164 | }
165 |
166 | func detectNetworkInterfaces() []string {
167 | detectedInterfaces := []string{}
168 |
169 | interfaces, _ := net.Interfaces()
170 | for _, i := range interfaces {
171 | // Skip loopback interfaces
172 | if !strings.HasPrefix(i.Name, "lo") {
173 | detectedInterfaces = append(detectedInterfaces, i.Name)
174 | }
175 | }
176 |
177 | return detectedInterfaces
178 | }
179 |
180 | func findModuleName(devicePath string) string {
181 | moduleFilename := filepath.Join(devicePath, "driver", "module")
182 | if _, err := os.Stat(moduleFilename); err != nil {
183 | return ""
184 | }
185 |
186 | realFilename, err := os.Readlink(moduleFilename)
187 | if err != nil {
188 | return ""
189 | }
190 |
191 | return filepath.Base(realFilename)
192 | }
193 |
--------------------------------------------------------------------------------
/cmd/init/flake.nix.txt:
--------------------------------------------------------------------------------
1 | {
2 | description = "A basic NixOS configuration";
3 |
4 | inputs = {
5 | %s
6 | };
7 |
8 | outputs = {nixpkgs, ...}: {
9 | # Change `my-nixos` to your desired hostname or machine name.
10 | nixosConfigurations.my-nixos = nixpkgs.lib.nixosSystem {
11 | modules = [
12 | ./configuration.nix
13 | ];
14 | };
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/init/hardware_configuration.nix.txt:
--------------------------------------------------------------------------------
1 | # Do not modify this file! It was generated by `nixos init`
2 | # and may be overwritten by future invocations. Please make
3 | # changes to your configuration.nix instead.
4 |
5 | {
6 | config,
7 | lib,
8 | pkgs,
9 | modulesPath,
10 | ...
11 | }: {
12 | imports = [
13 | %s
14 | ];
15 |
16 | boot.initrd.availableKernelModules = [%s];
17 | boot.initrd.kernelModules = [%s];
18 | boot.kernelModules = [%s];
19 | boot.extraModulePackages = [%s];
20 |
21 | # Filesystems
22 | %s
23 | # Swap devices
24 | %s
25 |
26 | # Enable DHCP on each ethernet and wireless interface. In case of scripted networking
27 | # (the default) this is the recommended approach. When using systemd-networkd it's
28 | # still possible to use this option, but it's recommended to use it in conjunction
29 | # with explicit per-interface declarations with `networking.interfaces..useDHCP`.
30 | networking.useDHCP = lib.mkDefault true;
31 | %s
32 | # Other config
33 | %s
34 | }
35 |
--------------------------------------------------------------------------------
/cmd/init/init.go:
--------------------------------------------------------------------------------
1 | package init
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/nix-community/nixos-cli/internal/build"
9 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
10 | "github.com/nix-community/nixos-cli/internal/cmd/utils"
11 | "github.com/nix-community/nixos-cli/internal/logger"
12 | "github.com/nix-community/nixos-cli/internal/settings"
13 | "github.com/nix-community/nixos-cli/internal/system"
14 | "github.com/spf13/cobra"
15 | )
16 |
17 | func InitCommand() *cobra.Command {
18 | opts := cmdOpts.InitOpts{}
19 |
20 | cmd := cobra.Command{
21 | Use: "init",
22 | Short: "Initialize a NixOS configuration",
23 | Long: "Initialize a NixOS configuration template and/or hardware options.",
24 | Args: func(cmd *cobra.Command, args []string) error {
25 | if !filepath.IsAbs(opts.Root) {
26 | return fmt.Errorf("--root must be an absolute path")
27 | }
28 | return nil
29 | },
30 | RunE: func(cmd *cobra.Command, args []string) error {
31 | return cmdUtils.CommandErrorHandler(initMain(cmd, &opts))
32 | },
33 | }
34 |
35 | cmdUtils.SetHelpFlagText(&cmd)
36 |
37 | cmd.Flags().StringVarP(&opts.Directory, "dir", "d", "/etc/nixos", "Directory `path` in root to write to")
38 | cmd.Flags().BoolVarP(&opts.ForceWrite, "force", "f", false, "Force generation of all configuration files")
39 | cmd.Flags().BoolVarP(&opts.NoFSGeneration, "no-fs", "n", false, "Do not generate 'fileSystem' options configuration")
40 | cmd.Flags().StringVarP(&opts.Root, "root", "r", "/", "Treat `path` as the root directory")
41 | cmd.Flags().BoolVarP(&opts.ShowHardwareConfig, "show-hardware-config", "s", false, "Print hardware config to stdout and exit")
42 |
43 | return &cmd
44 | }
45 |
46 | func initMain(cmd *cobra.Command, opts *cmdOpts.InitOpts) error {
47 | log := logger.FromContext(cmd.Context())
48 | cfg := settings.FromContext(cmd.Context())
49 | s := system.NewLocalSystem(log)
50 |
51 | virtType := determineVirtualisationType(s, log)
52 |
53 | log.Step("Generating hardware-configuration.nix...")
54 |
55 | hwConfigNixText, err := generateHwConfigNix(s, log, cfg, virtType, opts)
56 | if err != nil {
57 | log.Errorf("failed to generate hardware-configuration.nix: %v", err)
58 | return err
59 | }
60 |
61 | if opts.ShowHardwareConfig {
62 | fmt.Println(hwConfigNixText)
63 | return nil
64 | }
65 |
66 | log.Step("Generating configuration.nix...")
67 |
68 | configNixText, err := generateConfigNix(log, cfg, virtType)
69 | if err != nil {
70 | log.Errorf("failed to generate configuration.nix: %v", err)
71 | }
72 |
73 | log.Step("Writing configuration...")
74 |
75 | configDir := filepath.Join(opts.Root, opts.Directory)
76 | err = os.MkdirAll(configDir, 0o755)
77 | if err != nil {
78 | log.Errorf("failed to create %v: %v", configDir, err)
79 | return err
80 | }
81 |
82 | if buildOpts.Flake == "true" {
83 | flakeNixText := generateFlakeNix()
84 | flakeNixFilename := filepath.Join(configDir, "flake.nix")
85 | log.Infof("writing %v", flakeNixFilename)
86 |
87 | if _, err := os.Stat(flakeNixFilename); err == nil {
88 | if opts.ForceWrite {
89 | log.Warn("overwriting existing flake.nix")
90 | } else {
91 | log.Error("not overwriting existing flake.nix since --force was not specified, exiting")
92 | return nil
93 | }
94 | }
95 |
96 | err = os.WriteFile(flakeNixFilename, []byte(flakeNixText), 0o644)
97 | if err != nil {
98 | log.Errorf("failed to write %v: %v", flakeNixFilename, err)
99 | return err
100 | }
101 | }
102 |
103 | configNixFilename := filepath.Join(configDir, "configuration.nix")
104 | log.Infof("writing %v", configNixFilename)
105 | if _, err := os.Stat(configNixFilename); err == nil {
106 | if opts.ForceWrite {
107 | log.Warn("overwriting existing configuration.nix")
108 | } else {
109 | log.Error("not overwriting existing configuration.nix since --force was not specified, exiting")
110 | return nil
111 | }
112 | }
113 | err = os.WriteFile(configNixFilename, []byte(configNixText), 0o644)
114 | if err != nil {
115 | log.Errorf("failed to write %v: %v", configNixFilename, err)
116 | return err
117 | }
118 |
119 | hwConfigNixFilename := filepath.Join(configDir, "hardware-configuration.nix")
120 | log.Infof("writing %v", hwConfigNixFilename)
121 | if _, err := os.Stat(hwConfigNixFilename); err == nil {
122 | log.Warn("overwriting existing hardware-configuration.nix")
123 | }
124 | err = os.WriteFile(hwConfigNixFilename, []byte(hwConfigNixText), 0o644)
125 | if err != nil {
126 | log.Errorf("failed to write %v: %v", hwConfigNixFilename, err)
127 | return err
128 | }
129 |
130 | return nil
131 | }
132 |
--------------------------------------------------------------------------------
/cmd/manual/manual.go:
--------------------------------------------------------------------------------
1 | package manual
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 | "strings"
8 |
9 | "github.com/nix-community/nixos-cli/internal/cmd/utils"
10 | "github.com/nix-community/nixos-cli/internal/constants"
11 | "github.com/nix-community/nixos-cli/internal/logger"
12 | "github.com/nix-community/nixos-cli/internal/system"
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | func ManualCommand() *cobra.Command {
17 | cmd := cobra.Command{
18 | Use: "manual",
19 | Short: "Open the NixOS manual",
20 | Long: "Open the NixOS manual in a browser.",
21 | Args: cobra.NoArgs,
22 | RunE: func(cmd *cobra.Command, _ []string) error {
23 | return cmdUtils.CommandErrorHandler(manualMain(cmd))
24 | },
25 | }
26 |
27 | cmdUtils.SetHelpFlagText(&cmd)
28 |
29 | return &cmd
30 | }
31 |
32 | const (
33 | localManualFile = constants.CurrentSystem + "/sw/share/doc/nixos/index.html"
34 | manualURL = "https://nixos.org/manual/nixos/stable"
35 | )
36 |
37 | func manualMain(cmd *cobra.Command) error {
38 | log := logger.FromContext(cmd.Context())
39 | s := system.NewLocalSystem(log)
40 |
41 | if !s.IsNixOS() {
42 | log.Error("this command is only supported on NixOS systems")
43 | return nil
44 | }
45 |
46 | url := localManualFile
47 | if _, err := os.Stat(url); err != nil {
48 | log.Error("local documentation is not available, opening manual for current NixOS stable version")
49 | url = manualURL
50 | }
51 |
52 | var openCommand string
53 |
54 | browsers := strings.Split(os.Getenv("BROWSERS"), ":")
55 | for _, browser := range browsers {
56 | if p, err := exec.LookPath(browser); err == nil && p != "" {
57 | openCommand = p
58 | break
59 | }
60 | }
61 |
62 | defaultCommands := []string{"xdg-open", "w3m", "open"}
63 | if openCommand == "" {
64 | for _, cmd := range defaultCommands {
65 | if p, err := exec.LookPath(cmd); err == nil && p != "" {
66 | openCommand = p
67 | break
68 | }
69 | }
70 |
71 | if openCommand == "" {
72 | msg := "unable to locate suitable browser to open manual, exiting"
73 | log.Error(msg)
74 | return fmt.Errorf("%v", msg)
75 | }
76 | }
77 |
78 | log.Infof("opening manual using %v", openCommand)
79 | err := exec.Command(openCommand, url).Run()
80 | if err != nil {
81 | log.Errorf("failed to open manual: %v", err)
82 | return err
83 | }
84 |
85 | return nil
86 | }
87 |
--------------------------------------------------------------------------------
/cmd/option/cache.go:
--------------------------------------------------------------------------------
1 | package option
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "path/filepath"
7 | "strings"
8 |
9 | "github.com/nix-community/nixos-cli/internal/configuration"
10 | "github.com/nix-community/nixos-cli/internal/constants"
11 | "github.com/nix-community/nixos-cli/internal/system"
12 | )
13 |
14 | const (
15 | flakeOptionsCacheExpr = `let
16 | flake = builtins.getFlake "%s";
17 | system = flake.nixosConfigurations."%s";
18 | inherit (system) pkgs;
19 | inherit (pkgs) lib;
20 |
21 | optionsList' = lib.optionAttrSetToDocList system.options;
22 | optionsList = builtins.filter (v: v.visible && !v.internal) optionsList';
23 |
24 | jsonFormat = pkgs.formats.json {};
25 | in
26 | jsonFormat.generate "options-cache.json" optionsList
27 | `
28 | legacyOptionsCacheExpr = `let
29 | system = import {};
30 | inherit (system) pkgs;
31 | inherit (pkgs) lib;
32 |
33 | optionsList' = lib.optionAttrSetToDocList system.options;
34 | optionsList = builtins.filter (v: v.visible && !v.internal) optionsList';
35 |
36 | jsonFormat = pkgs.formats.json {};
37 | in
38 | jsonFormat.generate "options-cache.json" optionsList
39 | `
40 | )
41 |
42 | var prebuiltOptionCachePath = filepath.Join(constants.CurrentSystem, "etc", "nixos-cli", "options-cache.json")
43 |
44 | func buildOptionCache(s system.CommandRunner, cfg configuration.Configuration) (string, error) {
45 | argv := []string{"nix-build", "--no-out-link", "--expr"}
46 |
47 | switch v := cfg.(type) {
48 | case *configuration.FlakeRef:
49 | argv = append(argv, fmt.Sprintf(flakeOptionsCacheExpr, v.URI, v.System))
50 | case *configuration.LegacyConfiguration:
51 | argv = append(argv, legacyOptionsCacheExpr)
52 | for _, v := range v.Includes {
53 | argv = append(argv, "-I", v)
54 | }
55 | }
56 |
57 | cmd := system.NewCommand(argv[0], argv[1:]...)
58 |
59 | var stdout bytes.Buffer
60 | var stderr bytes.Buffer
61 | cmd.Stdout = &stdout
62 | cmd.Stderr = &stderr
63 |
64 | _, err := s.Run(cmd)
65 | if err != nil {
66 | return "", err
67 | }
68 |
69 | return strings.TrimSpace(stdout.String()), nil
70 | }
71 |
--------------------------------------------------------------------------------
/cmd/option/completion.go:
--------------------------------------------------------------------------------
1 | package option
2 |
3 | import (
4 | "os"
5 | "strings"
6 |
7 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
8 | "github.com/nix-community/nixos-cli/internal/configuration"
9 | "github.com/nix-community/nixos-cli/internal/logger"
10 | "github.com/nix-community/nixos-cli/internal/settings"
11 | "github.com/nix-community/nixos-cli/internal/system"
12 | "github.com/spf13/cobra"
13 | "github.com/water-sucks/optnix/option"
14 | )
15 |
16 | func loadOptions(log *logger.Logger, cfg *settings.Settings, includes []string) (option.NixosOptionSource, error) {
17 | s := system.NewLocalSystem(log)
18 |
19 | nixosConfig, err := configuration.FindConfiguration(log, cfg, includes, false)
20 | if err != nil {
21 | log.Errorf("failed to find configuration: %v", err)
22 | return nil, err
23 | }
24 |
25 | // Always use cache for completion if available.
26 | useCache := true
27 | _, err = os.Stat(prebuiltOptionCachePath)
28 | if err != nil {
29 | log.Warnf("error accessing prebuilt option cache: %v", err)
30 | useCache = false
31 | }
32 |
33 | optionsFileName := prebuiltOptionCachePath
34 | if !useCache {
35 | log.Info("building options list")
36 | f, err := buildOptionCache(s, nixosConfig)
37 | if err != nil {
38 | log.Errorf("failed to build option list: %v", err)
39 | return nil, err
40 | }
41 | optionsFileName = f
42 | }
43 |
44 | optionsFile, err := os.Open(optionsFileName)
45 | if err != nil {
46 | log.Errorf("failed to open options file %v: %v", optionsFileName, err)
47 | return nil, err
48 | }
49 | defer func() { _ = optionsFile.Close() }()
50 |
51 | options, err := option.LoadOptions(optionsFile)
52 | if err != nil {
53 | log.Errorf("failed to load options: %v", err)
54 | return nil, err
55 | }
56 |
57 | return options, nil
58 | }
59 |
60 | func OptionsCompletionFunc(opts *cmdOpts.OptionOpts) cobra.CompletionFunc {
61 | return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
62 | log := logger.FromContext(cmd.Context())
63 | cfg := settings.FromContext(cmd.Context())
64 |
65 | if len(args) != 0 {
66 | return []string{}, cobra.ShellCompDirectiveNoFileComp
67 | }
68 |
69 | options, err := loadOptions(log, cfg, opts.NixPathIncludes)
70 | if err != nil {
71 | return []string{}, cobra.ShellCompDirectiveNoFileComp
72 | }
73 |
74 | completions := []string{}
75 | for _, v := range options {
76 | if toComplete == v.Name {
77 | return []string{v.Name}, cobra.ShellCompDirectiveNoFileComp
78 | }
79 |
80 | if strings.HasPrefix(v.Name, toComplete) {
81 | completions = append(completions, v.Name)
82 | }
83 | }
84 |
85 | return completions, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/cmd/root/aliases.go:
--------------------------------------------------------------------------------
1 | package root
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 |
8 | "github.com/nix-community/nixos-cli/internal/utils"
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | func addAliasCmd(parent *cobra.Command, alias string, args []string) error {
13 | displayedArgs := utils.EscapeAndJoinArgs(args)
14 | description := fmt.Sprintf("Alias for `%v`.", displayedArgs)
15 |
16 | existingCommands := parent.Commands()
17 | for _, v := range existingCommands {
18 | if v.Name() == alias {
19 | return fmt.Errorf("alias conflicts with existing builtin command")
20 | }
21 | }
22 |
23 | if !parent.ContainsGroup("aliases") {
24 | parent.AddGroup(&cobra.Group{
25 | ID: "aliases",
26 | Title: "Aliases",
27 | })
28 | }
29 |
30 | cmd := &cobra.Command{
31 | Use: alias,
32 | Short: description,
33 | Long: description,
34 | GroupID: "aliases",
35 | DisableFlagParsing: true,
36 | RunE: func(cmd *cobra.Command, passedArgs []string) error {
37 | fullArgsList := append(args, passedArgs...)
38 |
39 | root := cmd.Root()
40 | root.SetArgs(fullArgsList)
41 | return root.Execute()
42 | },
43 | ValidArgsFunction: func(cmd *cobra.Command, passedArgs []string, toComplete string) ([]string, cobra.ShellCompDirective) {
44 | // HACK: So this is a rather lazy way of implementing completion for aliases.
45 | // I couldn't figure out how to get completions from the flag, so I decided
46 | // to just run the hidden completion command with the resolved arguments
47 | // and anything else that was passed. This should be negligible from a
48 | // performance perspective, but it's definitely a piece of shit.
49 | // Also, if you know, you know.
50 |
51 | // evil completion command hacking
52 | completionArgv := []string{os.Args[0], "__complete"} // what the fuck?
53 | completionArgv = append(completionArgv, args...)
54 | completionArgv = append(completionArgv, passedArgs...)
55 | completionArgv = append(completionArgv, toComplete)
56 |
57 | completionCmd := exec.Command(completionArgv[0], completionArgv[1:]...)
58 | completionCmd.Stdout = os.Stdout
59 | completionCmd.Stderr = os.Stderr
60 |
61 | // The completion command should always run.
62 | if err := completionCmd.Run(); err != nil {
63 | cobra.CompDebugln("failed to run completion command: "+err.Error(), true)
64 | os.Exit(1)
65 | }
66 |
67 | os.Exit(0)
68 |
69 | return []string{}, cobra.ShellCompDirectiveNoFileComp
70 | },
71 | }
72 |
73 | parent.AddCommand(cmd)
74 |
75 | return nil
76 | }
77 |
--------------------------------------------------------------------------------
/cmd/root/root.go:
--------------------------------------------------------------------------------
1 | package root
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/fatih/color"
9 | buildVars "github.com/nix-community/nixos-cli/internal/build"
10 | "github.com/nix-community/nixos-cli/internal/constants"
11 | "github.com/nix-community/nixos-cli/internal/logger"
12 | "github.com/nix-community/nixos-cli/internal/settings"
13 | "github.com/spf13/cobra"
14 |
15 | "github.com/nix-community/nixos-cli/internal/cmd/opts"
16 |
17 | applyCmd "github.com/nix-community/nixos-cli/cmd/apply"
18 | completionCmd "github.com/nix-community/nixos-cli/cmd/completion"
19 | enterCmd "github.com/nix-community/nixos-cli/cmd/enter"
20 | featuresCmd "github.com/nix-community/nixos-cli/cmd/features"
21 | generationCmd "github.com/nix-community/nixos-cli/cmd/generation"
22 | infoCmd "github.com/nix-community/nixos-cli/cmd/info"
23 | initCmd "github.com/nix-community/nixos-cli/cmd/init"
24 | installCmd "github.com/nix-community/nixos-cli/cmd/install"
25 | manualCmd "github.com/nix-community/nixos-cli/cmd/manual"
26 | optionCmd "github.com/nix-community/nixos-cli/cmd/option"
27 | replCmd "github.com/nix-community/nixos-cli/cmd/repl"
28 | )
29 |
30 | const helpTemplate = `Usage:{{if .Runnable}}
31 | {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
32 | {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
33 |
34 | Aliases:
35 | {{.NameAndAliases}}{{end}}{{if .HasExample}}
36 |
37 | Examples:
38 | {{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
39 |
40 | Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
41 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{if not .AllChildCommandsHaveGroup}}
42 |
43 | Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
44 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{range $group := .Groups}}
45 |
46 | {{.Title}}:{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
47 | {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
48 |
49 | Flags:
50 | {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}
51 | `
52 |
53 | func mainCommand() (*cobra.Command, error) {
54 | opts := cmdOpts.MainOpts{}
55 |
56 | log := logger.NewLogger()
57 | cmdCtx := logger.WithLogger(context.Background(), log)
58 |
59 | configLocation := os.Getenv("NIXOS_CLI_CONFIG")
60 | if configLocation == "" {
61 | configLocation = constants.DefaultConfigLocation
62 | }
63 |
64 | cfg, err := settings.ParseSettings(configLocation)
65 | if err != nil {
66 | log.Error(err)
67 | log.Warn("proceeding with defaults only, you have been warned")
68 | cfg = settings.NewSettings()
69 | }
70 |
71 | errs := cfg.Validate()
72 | for _, err := range errs {
73 | log.Warn(err.Error())
74 | }
75 |
76 | cmdCtx = settings.WithConfig(cmdCtx, cfg)
77 |
78 | cmd := cobra.Command{
79 | Use: "nixos {command} [flags]",
80 | Short: "nixos-cli",
81 | Long: "A tool for managing NixOS installations",
82 | Version: buildVars.Version,
83 | SilenceUsage: true,
84 | SuggestionsMinimumDistance: 1,
85 | CompletionOptions: cobra.CompletionOptions{
86 | HiddenDefaultCmd: true,
87 | },
88 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
89 | for key, value := range opts.ConfigValues {
90 | err := cfg.SetValue(key, value)
91 | if err != nil {
92 | return fmt.Errorf("failed to set %v: %w", key, err)
93 | }
94 | }
95 |
96 | errs := cfg.Validate()
97 | for _, err := range errs {
98 | log.Warn(err.Error())
99 | }
100 |
101 | // Now that we have the real color settings from parsing
102 | // the configuration and command-line arguments, set it.
103 | //
104 | // Precedence of color settings:
105 | // 1. -C flag -> true
106 | // 2. NO_COLOR=1 -> false, fatih/color already takes this into account
107 | // 3. `color` setting from config (default: true)
108 | if opts.ColorAlways {
109 | color.NoColor = false
110 | log.RefreshColorPrefixes()
111 | } else if os.Getenv("NO_COLOR") == "" {
112 | color.NoColor = !cfg.UseColor
113 | log.RefreshColorPrefixes()
114 | }
115 |
116 | return nil
117 | },
118 | }
119 |
120 | cmd.SetContext(cmdCtx)
121 |
122 | cmd.SetHelpCommand(&cobra.Command{Hidden: true})
123 | cmd.SetUsageTemplate(helpTemplate)
124 |
125 | boldRed := color.New(color.FgRed).Add(color.Bold)
126 | cmd.SetErrPrefix(boldRed.Sprint("error:"))
127 |
128 | cmd.Flags().BoolP("help", "h", false, "Show this help menu")
129 | cmd.Flags().BoolP("version", "v", false, "Display version information")
130 |
131 | cmd.PersistentFlags().BoolVar(&opts.ColorAlways, "color-always", false, "Always color output when possible")
132 | cmd.PersistentFlags().StringToStringVar(&opts.ConfigValues, "config", map[string]string{}, "Set a configuration `key=value`")
133 |
134 | _ = cmd.RegisterFlagCompletionFunc("config", settings.CompleteConfigFlag)
135 |
136 | cmd.AddCommand(applyCmd.ApplyCommand(cfg))
137 | cmd.AddCommand(completionCmd.CompletionCommand())
138 | cmd.AddCommand(enterCmd.EnterCommand())
139 | cmd.AddCommand(featuresCmd.FeatureCommand())
140 | cmd.AddCommand(generationCmd.GenerationCommand())
141 | cmd.AddCommand(infoCmd.InfoCommand())
142 | cmd.AddCommand(initCmd.InitCommand())
143 | cmd.AddCommand(installCmd.InstallCommand())
144 | cmd.AddCommand(manualCmd.ManualCommand())
145 | cmd.AddCommand(optionCmd.OptionCommand())
146 | cmd.AddCommand(replCmd.ReplCommand())
147 |
148 | for alias, resolved := range cfg.Aliases {
149 | err := addAliasCmd(&cmd, alias, resolved)
150 | if err != nil {
151 | log.Warnf("failed to add alias '%v': %v", alias, err.Error())
152 | }
153 | }
154 |
155 | return &cmd, nil
156 | }
157 |
158 | func Execute() {
159 | cmd, err := mainCommand()
160 | if err != nil {
161 | os.Exit(1)
162 | }
163 |
164 | if err = cmd.Execute(); err != nil {
165 | os.Exit(1)
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | {
2 | pkgs ? import {},
3 | lib ? pkgs.lib,
4 | }: let
5 | flake-self =
6 | (
7 | import
8 | (
9 | let
10 | lock = builtins.fromJSON (builtins.readFile ./flake.lock);
11 | in
12 | fetchTarball {
13 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
14 | sha256 = lock.nodes.flake-compat.locked.narHash;
15 | }
16 | )
17 | {src = ./.;}
18 | )
19 | .outputs;
20 |
21 | revision = (builtins.fetchGit ./.).rev;
22 | in {
23 | nixos = pkgs.callPackage ./package.nix {
24 | flake = true;
25 | inherit revision;
26 | };
27 |
28 | nixosLegacy = pkgs.callPackage ./package.nix {
29 | flake = false;
30 | inherit revision;
31 | };
32 |
33 | module = lib.modules.importApply ./module.nix flake-self;
34 | }
35 |
--------------------------------------------------------------------------------
/doc/.gitignore:
--------------------------------------------------------------------------------
1 | book
2 |
--------------------------------------------------------------------------------
/doc/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | authors = ["Varun Narravula"]
3 | language = "en"
4 | multilingual = false
5 | src = "src"
6 | title = "nixos-cli"
7 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-enter.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-ENTER(1)
2 |
3 | # NAME
4 |
5 | nixos-enter - enter a NixOS chroot environment
6 |
7 | # SYNOPSIS
8 |
9 | *nixos enter* [options] [-- ARGS...]
10 |
11 | # DESCRIPTION
12 |
13 | Enter a chroot environment rooted at the specified NixOS installation directory.
14 | This is primarily used for post-installation repair, debugging, or activating a
15 | configuration in an isolated environment.
16 |
17 | By default, the command enters _/mnt_ unless another root is specified with
18 | *--root*.
19 |
20 | You can execute commands within the chrooted shell using *--command* or by
21 | passing positional arguments after a double dash (`--`).
22 |
23 | # EXAMPLES
24 |
25 | Enter a chroot in _/path_ and get a login root shell:
26 |
27 | *nixos enter --root /path*
28 |
29 | Enter a chroot rooted at _/mnt_ by default, and run a single command (no shell):
30 |
31 | *nixos enter -- ls /etc/nixos*
32 |
33 | Enter a different system root with a custom system derivation closure path:
34 |
35 | *nixos enter --system /nix/store/NIX_STORE_HASH/nixos-system...*
36 |
37 | Run a command in a chrooted Bash shell and exit:
38 |
39 | *nixos enter --command "nixos-rebuild switch"*
40 |
41 | # OPTIONS
42 |
43 | *-c*, *--command*
44 | Execute the provided *STRING* in a Bash login shell after entering the
45 | chroot environment.
46 |
47 | Takes precedence over positional arguments for the command to execute.
48 |
49 | *-r*, *--root*
50 | Specify the root *PATH* of the NixOS system to enter.
51 |
52 | Default: */mnt*
53 |
54 | *--system*
55 | Manually specify the NixOS system configuration closure *PATH* to activate
56 | inside the chroot.
57 |
58 | *-s*, *--silent*
59 | Suppress output from the activation scripts and other spurious logging.
60 |
61 | *-v*, *--verbose*
62 | Show verbose logging and diagnostic output during entry and activation.
63 |
64 | *-h*, *--help*
65 | Show the help message for this command.
66 |
67 | # ARGUMENTS
68 |
69 | *[ARGS...]*
70 | If provided, arguments are interpreted as the command to execute in the
71 | environment. Must be preceded by a double dash (`--`) to separate invoked
72 | command options from *nixos enter* options.
73 |
74 | If *--command* is specified, these arguments will be ignored.
75 |
76 | # SEE ALSO
77 |
78 | *nixos-apply*(1)
79 |
80 | *nixos-install*(1)
81 |
82 | *chroot*(1)
83 |
84 | # AUTHORS
85 |
86 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
87 | details.
88 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-env.5.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-ENV(5)
2 |
3 | # NAME
4 |
5 | nixos-cli-env - environment variables used by *nixos-cli*
6 |
7 | # DESCRIPTION
8 |
9 | The *nixos-cli* tool is influenced by a number of environment variables. These
10 | allow the user to configure the CLI's behavior at runtime, such as:
11 |
12 | - Default inputs
13 | - Formatting preferences
14 |
15 | Among other things.
16 |
17 | # ENVIRONMENT VARIABLES
18 |
19 | *NO_COLOR*
20 | Disable colored output in *nixos-cli*.
21 |
22 | Does not apply to terminal user interfaces (TUIs), which manage their own
23 | display logic.
24 |
25 | *NIXOS_CLI_CONFIG*
26 | Specify a custom path for the *nixos-cli* settings file.
27 |
28 | Default: */etc/nixos-cli/config.toml*
29 |
30 | *NIXOS_CONFIG*
31 | Defines the configuration that *nixos-cli* will operate on.
32 | Its meaning depends on whether the CLI is built with flake support.
33 |
34 | This environment variable takes precedence over the *config_location*
35 | setting if it is set.
36 |
37 | *Flake-enabled CLIs:*
38 | This must be a valid flake ref (e.g., */home/user/config* or
39 | *github:myorg/nixos-config#attr*).
40 |
41 | Flake refs without a local path may have slightly different behavior,
42 | such as not supporting implicit Git operations. Check relevant man pages
43 | for more information.
44 |
45 | If a flake ref is a path, it _MUST_ be absolute. Use *realpath(1)* if
46 | you must.
47 |
48 | Additionally, flake refs will usually be expanded when necessary.
49 | For example, the following flake ref:
50 |
51 | _github:water-sucks/nixed#CharlesWoodson_
52 |
53 | will get expanded to the following flake ref and attribute:
54 |
55 | _github:water-sucks/nixed#nixosConfigurations.CharlesWoodson_
56 |
57 | If the ref does not have text after the "#", then the NixOS system
58 | attribute name will be inferred to be the current system's hostname.
59 |
60 | *Legacy CLIs:*
61 | This can be a path to a configuration file directly
62 | (*configuration.nix*), or a directory containing a *default.nix*.
63 |
64 | Legacy configurations can also be sourced from the *$NIX_PATH*
65 | environment variable, if the _nixos-config=_ attribute is
66 | specified there.
67 |
68 | *NIXOS_CLI_DISABLE_STEPS*
69 | Disable showing visual steps with the logger. These "steps" get converted to
70 | information logs internally if this is set.
71 |
72 | Mostly useful for internal implementation, rather than for end-users.
73 |
74 | *NIXOS_CLI_DEBUG_MODE*
75 | Show log messages for when developing TUIs. Only useful for during
76 | development.
77 |
78 | # SEE ALSO
79 |
80 | *nixos-cli-config(5)*
81 |
82 | # AUTHORS
83 |
84 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
85 | details.
86 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-features.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-FEATURES(1)
2 |
3 | # NAME
4 |
5 | nixos features - show metadata about the application and configured options
6 |
7 | # SYNOPSIS
8 |
9 | *nixos features* [options]
10 |
11 | # DESCRIPTION
12 |
13 | The *nixos features* command displays metadata about the current build of the
14 | *nixos-cli* application. This includes version information, enabled features,
15 | environment details such as the detected Nix version, and relevant build-time
16 | configuration.
17 |
18 | This command is particularly useful for:
19 |
20 | - Understanding capabilities of the current *nixos-cli* binary
21 | - Diagnostics for reporting issues
22 | - Debugging issues with the current installation
23 |
24 | # OPTIONS
25 |
26 | *-j*, *--json*
27 | Output all metadata in machine-readable JSON format.
28 |
29 | *-h*, *--help*
30 | Show the help message for this command.
31 |
32 | # AUTHORS
33 |
34 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
35 | details.
36 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-generation-delete.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-GENERATION-DELETE(1)
2 |
3 | # NAME
4 |
5 | nixos generation delete - delete NixOS generations from this system
6 |
7 | # SYNOPSIS
8 |
9 | *nixos generation delete* [options] [GEN...]
10 |
11 | # DESCRIPTION
12 |
13 | Delete NixOS generations from this system based on a flexible set of
14 | constraints.
15 |
16 | You can specify individual generation numbers or combine options to tailor which
17 | generations are deleted; the order of operations for these options is defined
18 | below the options and arguments.
19 |
20 | # EXAMPLES
21 |
22 | Delete all generations older than 30 days but keep generation #42:
23 |
24 | *nixos generation delete --older-than '30d' --keep 42*
25 |
26 | Delete generations from 42-69 (inclusive) and ensure that 5 generations remain:
27 |
28 | *nixos generation delete --from 42 --to 69 --min 5*
29 |
30 | Delete all generations starting from 25 and ensure that 32 is always kept; also
31 | delete 22 explicitly at the same time:
32 |
33 | *nixos generation delete 22 --from 25 --keep 25*
34 |
35 | # OPTIONS
36 |
37 | *-a*, *--all*
38 | Delete all generations except the current one.
39 |
40 | *-f*, *--from*
41 | Delete all generations after generation number *GEN*, inclusive.
42 |
43 | This will go all the way up to the latest generation number if not
44 | accompanied by the *--to* parameter, which sets an upper bound.
45 |
46 | *-h*, *--help*
47 | Show the help message for this command.
48 |
49 | *-k*, *--keep*
50 | Always keep the specified generation number *GEN*. This option can be
51 | specified multiple times.
52 |
53 | *-m*, *--min*
54 | Ensure that a minimum of *NUM* generations _always_ exists.
55 |
56 | *-o*, *--older-than*
57 | Delete all generations older than *DURATION*. The *DURATION* value is a
58 | *systemd.time(7)*-formatted time span, such as *"30d 2h 1m"*.
59 |
60 | For more information, see the *systemd.time(7)* man page.
61 |
62 | *-t*, *--to*
63 | Delete all generations until generation number *GEN*, inclusive.
64 |
65 | This will go all the way down to the earliest generation number if not
66 | accompanied by the *--from* parameter, which sets a lower bound.
67 |
68 | *-v*, *--verbose*
69 | Enable verbose logging.
70 |
71 | *-y*, *--yes*
72 | Automatically confirm generation deletion without any interactive prompt.
73 |
74 | # ARGUMENTS
75 |
76 | *[GEN]...*
77 | One or more specific generation numbers to delete. These can be used
78 | alongside the options for more fine-grained control, but are optional.
79 |
80 |
81 | Options and arguments can be combined ad-hoc to create complex filtering
82 | constraints.
83 |
84 | # ORDER OF OPERATIONS
85 |
86 | The order of evaluating generations to delete vs. which ones to keep is this,
87 | from most prioritized to least prioritized:
88 |
89 | - *--min*
90 | - *--keep*
91 | - *[GEN...]* positional args
92 | - *--all*
93 | - *--from* + *--to*
94 | - *--older-than*
95 |
96 | And for any range where the generation numbers to keep can be ambiguous, the
97 | most recent generations will be kept.
98 |
99 | # SEE ALSO
100 |
101 | *nixos-cli-generation(1)*
102 |
103 | *systemd.time(7)*
104 |
105 | # AUTHORS
106 |
107 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
108 | details.
109 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-generation-diff.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-GENERATION-DIFF(1)
2 |
3 | # NAME
4 |
5 | nixos generation diff - display differences between two generations
6 |
7 | # SYNOPSIS
8 |
9 | *nixos generation diff* [BEFORE] [AFTER] [options]
10 |
11 | # DESCRIPTION
12 |
13 | Compare two NixOS system generations and show which paths differ between them.
14 | This can help users understand what changed between deployments.
15 |
16 | Both generation numbers must exist and belong to the same profile.
17 |
18 | The output highlights differing store paths or system configuration files
19 | between the two specified generations.
20 |
21 | The diff command that is ran can be one of two options:
22 |
23 | - *nix store diff-closures* (default)
24 | - *nvd*, which has prettier output (if the setting _use_nvd_ is set and
25 | if it is installed)
26 |
27 | # OPTIONS
28 |
29 | *-h*, *--help*
30 | Show the help message for this command.
31 |
32 | *-v*, *--verbose*
33 | Enable verbose logging, including more detailed output of differing paths.
34 |
35 | # ARGUMENTS
36 |
37 | *BEFORE*
38 | Generation number to compare from.
39 |
40 | *AFTER*
41 | Generation number to compare against.
42 |
43 | # SEE ALSO
44 |
45 | *nixos-generation(1)*
46 |
47 | *nixos-cli-generation-list(1)*
48 |
49 | *nix3-store-diff-closures(1)*
50 |
51 | # AUTHORS
52 |
53 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
54 | details.
55 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-generation-list.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-GENERATION-LIST(1)
2 |
3 | # NAME
4 |
5 | nixos generation list - list available generations on a NixOS machine
6 |
7 | # SYNOPSIS
8 |
9 | *nixos generation list* [options]
10 |
11 | # DESCRIPTION
12 |
13 | List all generations in the current NixOS profile, along with relevant metadata
14 | such as generation number, timestamp, and description.
15 |
16 | This command provides both programmatic output formats and an interactive
17 | TUI that can be used to view generations with ease.
18 |
19 | By default, this command launches the TUI.
20 |
21 | - Use the arrow keys or _hjkl_ to navigate through generations.
22 | - Type _/_ to search by generation number or description.
23 | - Press __ to switch to a given generation.
24 | - Press __ to mark generations for deletion (except the current one).
25 | - Press _d_ to delete all marked generations.
26 | - Press __ or _q_ to exit.
27 |
28 | This interface is designed to make reviewing and managing system generations
29 | faster and more user-friendly.
30 |
31 | # EXAMPLES
32 |
33 | Extract just the generation numbers using *jq*:
34 |
35 | *nixos generation list -j | jq '.[].number'*
36 |
37 | List generations in table format without the interactive UI:
38 |
39 | *nixos generation list -t*
40 |
41 | Extract just the generation numbers using *cut* from table output:
42 |
43 | *nixos generation list -t | cut -d ' ' -f 1*
44 |
45 | # OPTIONS
46 |
47 | *-h*, *--help*
48 | Show the help message for this command.
49 |
50 | *-j*, *--json*
51 | Display the generation list in JSON format. Suitable for scripts or machine
52 | parsing.
53 |
54 | *-t*, *--table*
55 | Display the generation list in a *grep*-pable table format. Also suitable
56 | for scripts where JSON parsing is not available.
57 |
58 | # SEE ALSO
59 |
60 | *nixos-cli-generation-diff(1)*
61 |
62 | *nixos-cli-generation-delete(1)*
63 |
64 | *nixos-cli-generation-rollback(1)*
65 |
66 | *nixos-cli-generation-switch(1)*
67 |
68 | # AUTHORS
69 |
70 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
71 | details.
72 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-generation-rollback.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-GENERATION-ROLLBACK(1)
2 |
3 | # NAME
4 |
5 | nixos generation rollback - rollback to the previous NixOS generation
6 |
7 | # SYNOPSIS
8 |
9 | *nixos generation rollback* [options]
10 |
11 | # DESCRIPTION
12 |
13 | Rollback to the previous NixOS generation number, if it exists. Similar to
14 | *nixos generation switch*, except the generation number is not required, and
15 | as such, is a little more ergonomic to use in most situations where a rollback
16 | is required.
17 |
18 | *NOTE*: This only relies on the number to determine the last generation that was
19 | activated; as such, it does not actually rollback to the last generation
20 | number that was switched to, since there is no record of this.
21 |
22 | Useful for rolling back to a known good state or testing previous
23 | configurations.
24 |
25 | # OPTIONS
26 |
27 | *-d*, *--dry*
28 | Show what would be activated, but do not perform any actual activation.
29 |
30 | Equivalent to running *switch-to-configuration* manually with the
31 | *dry-activate* command.
32 |
33 | *-h*, *--help*
34 | Show the help message for this command.
35 |
36 | *-s*, *--specialisation*
37 | Activate a specific specialisation *NAME* within the selected generation.
38 |
39 | If the default specialisation is specified in the *nixos-cli* configuration
40 | for this generation number, and this option is not specified, it will switch
41 | to that specialisation automatically, rather than using the base one.
42 |
43 | *-v*, *--verbose*
44 | Show verbose logging during activation.
45 |
46 | *-y*, *--yes*
47 | Automatically confirm the generation switch, without prompting.
48 |
49 | # SEE ALSO
50 |
51 | *nixos-cli-generation-list*(1)
52 |
53 | *nixos-cli-generation-switch*(1)
54 |
55 | *nixos-cli-apply(1)*
56 |
57 | # AUTHORS
58 |
59 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
60 | details.
61 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-generation-switch.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-GENERATION-SWITCH(1)
2 |
3 | # NAME
4 |
5 | nixos generation switch - activate an arbitrary existing NixOS generation
6 |
7 | # SYNOPSIS
8 |
9 | *nixos generation switch* [GEN] [options]
10 |
11 | # DESCRIPTION
12 |
13 | Activate a specific, already-existing NixOS generation by its generation number.
14 |
15 | Useful for rolling back to a known good state or testing previous
16 | configurations.
17 |
18 | # EXAMPLES
19 |
20 | Switch to generation 42 with confirmation prompt:
21 |
22 | *nixos generation switch 42*
23 |
24 | Dry-run switch to generation 35:
25 |
26 | *nixos generation switch 35 -d*
27 |
28 | Switch to generation 18 and automatically confirm:
29 |
30 | *nixos generation switch 18 -y*
31 |
32 | Switch to a given specialisation within generation 25:
33 |
34 | *nixos generation switch 25 -s "minimal"*
35 |
36 | # OPTIONS
37 |
38 | *-d*, *--dry*
39 | Show what would be activated, but do not perform any actual activation.
40 |
41 | Equivalent to running *switch-to-configuration* manually with the
42 | *dry-activate* command.
43 |
44 | *-h*, *--help*
45 | Show the help message for this command.
46 |
47 | *-s*, *--specialisation*
48 | Activate a specific specialisation *NAME* within the selected generation.
49 |
50 | If the default specialisation is specified in the *nixos-cli* configuration
51 | for this generation number, and this option is not specified, it will switch
52 | to that specialisation automatically, rather than using the base one.
53 |
54 | *-v*, *--verbose*
55 | Show verbose logging during activation.
56 |
57 | *-y*, *--yes*
58 | Automatically confirm the generation switch, without prompting.
59 |
60 | # ARGUMENTS
61 |
62 | *[GEN]*
63 | The number of the generation to activate.
64 |
65 | This must be an existing generation in the selected NixOS profile.
66 |
67 | # SEE ALSO
68 |
69 | *nixos-cli-generation-list*(1)
70 |
71 | *nixos-cli-generation-rollback*(1)
72 |
73 | *nixos-cli-apply(1)*
74 |
75 | # AUTHORS
76 |
77 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
78 | details.
79 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-generation.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-GENERATION(1)
2 |
3 | # NAME
4 |
5 | nixos generation - manage NixOS generations on this machine
6 |
7 | # SYNOPSIS
8 |
9 | *nixos generation* [command] [options]
10 |
11 | # DESCRIPTION
12 |
13 | The *nixos generation* command provides subcommands for managing NixOS system
14 | generations.
15 |
16 | This is the other component to *nixos apply* that replaces the existing
17 | *nixos-rebuild(1)* command's functionality.
18 |
19 | Generations are snapshots of system configurations that can be listed,
20 | activated, compared, or deleted in the same way as any other Nix closure.
21 |
22 | As such, this command allows you to explore previous system states, switch
23 | between them, or inspect changes across generations.
24 |
25 | # EXAMPLES
26 |
27 | Examples are provided in each subcommand's respective man page.
28 |
29 | # COMMANDS
30 |
31 | *delete*
32 | Delete one or more generations from the specified profile based on a range
33 | of constraints.
34 |
35 | *diff*
36 | Show differences between two generations, such as package changes or option
37 | modifications.
38 |
39 | *list*
40 | List all generations available in the system profile.
41 |
42 | *rollback*
43 | Activate the generation prior to the current one.
44 |
45 | *switch*
46 | Activate a specified existing generation.
47 |
48 | # OPTIONS
49 |
50 | *-p*, *--profile*
51 | Specify the system profile *NAME* to operate on; this should contain all
52 | the generations that will be worked with.
53 |
54 | Default: *system*
55 |
56 | *-h*, *--help*
57 | Show the help message for this command.
58 |
59 | # SEE ALSO
60 |
61 | *nixos-cli-generation-delete*(1)
62 |
63 | *nixos-cli-generation-diff*(1)
64 |
65 | *nixos-cli-generation-list*(1)
66 |
67 | *nixos-cli-generation-rollback*(1)
68 |
69 | *nixos-cli-generation-switch*(1)
70 |
71 | *nixos-cli-apply(1)*
72 |
73 | # AUTHORS
74 |
75 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
76 | details.
77 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-info.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-INFO(1)
2 |
3 | # NAME
4 |
5 | nixos info - show information about the currently running NixOS generation
6 |
7 | # SYNOPSIS
8 |
9 | *nixos info* [options]
10 |
11 | # DESCRIPTION
12 |
13 | The *nixos info* command displays metadata about the currently active NixOS
14 | generation.
15 |
16 | This includes generation number, activation time, system profile, and
17 | configuration details.
18 |
19 | It can be useful for diagnostics, system reporting, or to confirming that a
20 | deployment has succeeded.
21 |
22 | Currently, this command only shows the generation number when using the
23 | _system_ profile, due to the fact that there is no reliable way to tell
24 | what profile is being used.
25 |
26 | # OPTIONS
27 |
28 | *-h*, *--help*
29 | Show the help message for this command.
30 |
31 | *-j*, *--json*
32 | Output the information as a structured JSON object.
33 |
34 | *-m*, *--markdown*
35 | Format the output as Markdown, useful for pasting into issue reports or
36 | documentation.
37 |
38 | # SEE ALSO
39 |
40 | *nixos-cli-generation-list(1)*
41 |
42 | # AUTHORS
43 |
44 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
45 | details.
46 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-init.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-INIT(1)
2 |
3 | # NAME
4 |
5 | nixos init - initialize a NixOS configuration template and/or hardware options
6 |
7 | # SYNOPSIS
8 |
9 | *nixos init* [options]
10 |
11 | # DESCRIPTION
12 |
13 | The *nixos init* command scaffolds a basic NixOS configuration in the specified
14 | root directory and/or updates hardware configurations depending on what is
15 | detected on the system.
16 |
17 | It is primarily used before a system installation to create an initial
18 | _configuration.nix_ and hardware settings, or after installation to
19 | automatically update a given hardware configuration.
20 |
21 | Two files are generated for all systems:
22 |
23 | _/path/to/configuration.nix_
24 | A main NixOS system configuration module. Only generated when it does not
25 | exist, unless *--force* is passed.
26 |
27 | _/path/to/hardware_configuration.nix_
28 | A module that sets NixOS configuration options based on the current
29 | hardware and filesystems that are detected. Examples of these types of
30 | configurations include
31 | - Kernel modules required for the hardware
32 | - Detected filesystems
33 | - Detected swap partitions
34 | - _initrd_ options for booting the system
35 |
36 | This gets regenerated, and shouldn't be touched much, since changes are
37 | overwritten each time this command is ran.
38 |
39 | Depending on if the CLI is flake-enabled, a _flake.nix_ file may also be
40 | generated for new configurations.
41 |
42 | # EXAMPLES
43 |
44 | Create a new NixOS configuration in /mnt for installation:
45 |
46 | *nixos init --root /mnt*
47 |
48 | Print a NixOS hardware configuration module to stdout and redirect to a file:
49 |
50 | *nixos init --show-hardware-config > path/to/hwconfig.nix*
51 |
52 | # OPTIONS
53 |
54 | *-d*, *--dir* _path_
55 | Directory inside the root where configuration files will be written.
56 |
57 | This gets concatenated to the *--root* option, so paths will take the form
58 | of _/mnt/etc/nixos_ if *--root* is _/mnt_
59 |
60 | Default: */etc/nixos*
61 |
62 | *-f*, *--force*
63 | Forcefully generate all configuration files, overwriting any existing ones
64 | for _configuration.nix_ (and _flake.nix_, if applicable).
65 |
66 | *-h*, *--help*
67 | Show the help message for this command.
68 |
69 | *-n*, *--no-fs*
70 | Skip generation of _fileSystems_ and _swapDevices_ configuration options.
71 |
72 | Useful if disk options are to be managed separately/manually, such as
73 | through external modules.
74 |
75 | *-r*, *--root*
76 | Treat *PATH* as the root directory of the system.
77 |
78 | Default: */*
79 |
80 | *-s*, *--show-hardware-config*
81 | Print the generated hardware configuration to stdout and exit.
82 |
83 | No files will be written when this option is used.
84 |
85 | # SEE ALSO
86 |
87 | *nixos-cli-install(1)*
88 |
89 | # AUTHORS
90 |
91 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
92 | details.
93 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-install.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-INSTALL(1)
2 |
3 | # NAME
4 |
5 | nixos install - install a NixOS system from a provided configuration
6 |
7 | # SYNOPSIS
8 |
9 | *nixos install* [FLAKE-REF] [options]
10 |
11 | # DESCRIPTION
12 |
13 | *nixos install* installs a NixOS system by:
14 |
15 | - Initialize the Nix store and copy an initial _nixos_ channel
16 | - Building and creating an initial generation on the target mountpoint
17 | - Installing a bootloader if configured
18 | - Setting a root password
19 |
20 | The installed configuration will depend on if the CLI is flake-enabled or not.
21 |
22 | If the CLI is flake-enabled:
23 | - The *[FLAKE-REF]* argument is _required_, and must point to a valid
24 | flake reference with a NixOS configuration.
25 | Otherwise:
26 | - The *$NIXOS_CONFIG* variable must point to a valid NixOS configuration
27 | module or a directory containing a _default.nix_ with the same.
28 | - OR the target root must have a file at _/etc/nixos/configuration.nix_
29 |
30 | The installation will take place relative to a specified root directory
31 | (defaults to `/mnt`). Mountpoints and all other filesystems must be mounted
32 | in the correct place prior to installation, or they will not exist on the
33 | target system.
34 |
35 | *nixos install* is idempotent, and as such, can be used to re-install broken
36 | NixOS systems without needing to wipe filesystems first, in most cases. That
37 | means this command can be re-run if problems arise during installation, and
38 | can also be ran on already-existing NixOS installations.
39 |
40 | In the future, remote installations will be supported. Look to the release page
41 | on GitHub for when this feature comes out.
42 |
43 | # EXMAPLES
44 |
45 | In order to create a fresh NixOS installation, mountpoints need to be set up
46 | beforehand.
47 |
48 | This block of commands will show a typical installation procedure, assuming the
49 | following simple partition layout on a UEFI system:
50 |
51 | /dev/sda1 :: EFI System Partition (512M, FAT32)
52 | /dev/sda2 :: NixOS root partition (ext4)
53 |
54 | Typically, a user will also need to obtain *nixos-cli* in a Nix shell, as it is
55 | not available on traditional NixOS install ISOs or Linux distributions, as of
56 | yet.
57 |
58 | Refer to the installation steps in the online manual to obtain the *nixos-cli*
59 | binary in a development shell.
60 |
61 | Installation steps:
62 |
63 | _$ mkfs.fat -F 32 /dev/sda1_
64 |
65 | _$ mkfs.ext4 /dev/sda2_
66 |
67 | _$ mount /dev/sda2 /mnt_
68 |
69 | _$ mkdir -p /mnt/boot_
70 |
71 | _$ mount /dev/sda1 /mnt/boot_
72 |
73 | _$ nixos init --root /mnt_
74 |
75 | # Change whatever you will need to in the configuration.
76 |
77 | # For flake-enabled CLIs, use the following invocation:
78 |
79 | _$ nixos install --root /mnt --flake /mnt/etc/nixos#_
80 |
81 | # For non-flake CLIs, use the following invocation:
82 |
83 | _$ nixos install --root /mnt_
84 |
85 | # Reboot
86 |
87 | Running *nixos enter --root /mnt* in order to inspect the new installation
88 | is also possible, assuming a successful installation.
89 |
90 | # ARGUMENTS
91 |
92 | *FLAKE-REF*
93 | Specify an explicit flake ref to evaluate options from. Only available
94 | on flake-enabled CLIs.
95 |
96 | See *nixos-config-env(5)* for the proper flake ref format.
97 |
98 | The system name is NOT inferred from the hostname, and must be provided
99 | explicitly after the #.
100 |
101 | There is no fallback, so if this argument is not provided on a flake-enabled
102 | CLI, the program will fail.
103 |
104 | # OPTIONS
105 |
106 | *-c*, *--channel*
107 | Use the derivation at *PATH* as the _nixos_ channel to copy to the target
108 | system.
109 |
110 | If not provided, then the existing _nixos_ channel for the root user on the
111 | running system will be copied instead. This is usually the case on live
112 | NixOS USBs or external media used for fresh installations.
113 |
114 | *-h*, *--help*
115 | Show the help message for this command.
116 |
117 | *--no-bootloader*
118 | Do not install the bootloader on the target device.
119 |
120 | For fresh installations, it is recommended not to enable this option.
121 | Proceed at your own risk.
122 |
123 | *--no-channel-copy*
124 | Do not copy a NixOS channel to the target system.
125 |
126 | This is useful for speeding up installations if the target NixOS channel
127 | already exists, or if using flake configurations that do not require
128 | Nix channels configured at all.
129 |
130 | Conflicts with *--channel*.
131 |
132 | *--no-root-passwd*
133 | Skip prompting to set the root password.
134 |
135 | Useful for non-interactive installation, such as in scripts.
136 |
137 | *-r*, *--root*
138 | Treat *DIR* as the root directory for installation.
139 |
140 | Default: */mnt*
141 |
142 | *-s*, *--system*
143 | Install the system from an already built system closure at *PATH*.
144 |
145 | This is useful for installing multiple systems from the same system closure,
146 | to avoid repeated _nix build_ calls.
147 |
148 | *-v*, *--verbose*
149 | Enable verbose logging.
150 |
151 | # NIX OPTIONS
152 |
153 | *nixos apply* accepts some Nix options and passes them through to their relevant
154 | Nix invocations.
155 |
156 | The following options are supported:
157 |
158 | - *--quiet*
159 | - *--print-build-logs*, *-L*
160 | - *--no-build-output*, *-Q*
161 | - *--show-trace*
162 | - *--keep-going*, *-k*
163 | - *--keep-failed*, *-K*
164 | - *--fallback*
165 | - *--refresh*
166 | - *--repair*
167 | - *--impure*
168 | - *--offline*
169 | - *--no-net*
170 | - *--max-jobs*, *-j*
171 | - *--cores*
172 | - *--log-format*
173 | - *--include*, *-I*
174 | - *--option* (single argument, separated by an = sign)
175 |
176 | *--option* is specified slightly differently; for *nixos-cli* to pass it through
177 | properly, pass the option key and value as a single argument, rather than as two
178 | separate arguments in the actual Nix CLI.
179 |
180 | The following options are supported on flake-enabled CLIs:
181 |
182 | - *--recreate-lock-file*
183 | - *--no-update-lock-file*
184 | - *--no-registries*, *--no-use-registries*
185 | - *--commit-lock-file*
186 | - *--update-input*
187 | - *--override-input*
188 |
189 | *--override-input* is specified slightly differently; for *nixos-cli* to pass it
190 | through properly, pass the input name and value as a single argument, rather
191 | than as two arguments in the actual Nix CLI.
192 |
193 | # SEE ALSO
194 |
195 | *nixos-cli-enter(1)*
196 |
197 | *nixos-cli-init(1)*
198 |
199 | *nix3-build*(1), *nix-build(1)*
200 |
201 | *nix-env(1)*
202 |
203 | *nixos-cli-env*(5)
204 |
205 | # AUTHORS
206 |
207 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
208 | details.
209 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-manual.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-MANUAL(1)
2 |
3 | # NAME
4 |
5 | nixos manual - open the NixOS manual in a browser
6 |
7 | # SYNOPSIS
8 |
9 | *nixos manual*
10 |
11 | # DESCRIPTION
12 |
13 | Opens the NixOS manual in your default web browser.
14 |
15 | This command is a convenience wrapper that launches the appropriate URL for the
16 | NixOS manual associated with your configuration. It is especially helpful for
17 | quickly referencing system configuration options and NixOS concepts.
18 |
19 | The manual opened is based on the version of Nixpkgs currently in use by your
20 | system, unless the manual is not present on the local system in question.
21 |
22 | # ENVIRONMENT
23 |
24 | The behavior of this command can be influenced by the *$BROWSERS* environment
25 | variable, which determines which browser is used to open the manual.
26 |
27 | If *$BROWSERS* is not set, the command attempts to fall back on a sensible
28 | system default (*xdg-open*).
29 |
30 | Examples of `BROWSER` usage:
31 |
32 | *BROWSER=firefox nixos manual*
33 |
34 | *BROWSER=brave nixos manual*
35 |
36 | *BROWSER=w3m nixos manual*
37 |
38 | # OPTIONS
39 |
40 | *-h*, *--help*
41 | Show the help message for this command.
42 |
43 | # SEE ALSO
44 |
45 | *xdg-open(1)*
46 |
47 | # AUTHORS
48 |
49 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
50 | details.
51 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-option-tui.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-OPTION-TUI(1)
2 |
3 | # NAME
4 |
5 | nixos option -i - query available NixOS options interactively
6 |
7 | # SYNOPSIS
8 |
9 | *nixos option -i* [QUERY]
10 |
11 | # DESCRIPTION
12 |
13 | *nixos option -i* is an interactive TUI search for NixOS commands.
14 |
15 | # WINDOW LAYOUT
16 |
17 | A purple border means that a given window is active. If a window is active, then
18 | its keybinds will work.
19 |
20 | The main windows are the:
21 |
22 | - Input/Result List Window
23 | - Preview Window
24 | - Help Window (this one)
25 | - Option Value Window
26 |
27 | ## Help Window
28 |
29 | Use the arrow keys or _h_, _j_, _k_, and _l_ to scroll around.
30 |
31 | __ or _q_ will close this help window.
32 |
33 | ## Option Input Window
34 |
35 | Type anything into the input box and all available options that match will be
36 | filtered into a list. Scroll this list with the up or down cursor keys, and the
37 | information for that option will show in the option preview window.
38 |
39 | __ moves to the option preview window.
40 |
41 | __ previews that option's current value, if it is able to be evaluated.
42 | This will toggle the option value window.
43 |
44 | ## Option Preview Window
45 |
46 | Use the cursor keys or _h_, _j_, _k_, and _l_ to scroll around.
47 |
48 | The input box is not updated when this window is active.
49 |
50 | __ will move back to the input window for searching.
51 |
52 | __ will also evaluate the value, if possible. This will toggle the option
53 | value window.
54 |
55 | ## Option Value Window
56 |
57 | Use the cursor keys or _h_, _j_, _k_, and _l_ to scroll around.
58 |
59 | __ or _q_ will close this window.
60 |
61 | # SEE ALSO
62 |
63 | *nixos-cli-option(1)*
64 |
65 | # AUTHORS
66 |
67 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
68 | details.
69 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-option.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-OPTION(1)
2 |
3 | # NAME
4 |
5 | nixos option - query available NixOS module options for this system
6 |
7 | # SYNOPSIS
8 |
9 | *nixos option* [options] [NAME]
10 |
11 | # DESCRIPTION
12 |
13 | Display documentation and values for available NixOS configuration options.
14 | This command can be used to inspect how modules influence the system and what
15 | values are currently set for individual options.
16 |
17 | The command will enter an interactive search mode if *--interactive* is passed.
18 | Otherwise, it expects a specific option name to display details for.
19 |
20 | If the option is found in non-interactive mode, then its details are displayed.
21 | Otherwise, similar options are searched for, and printed if they roughly
22 | match the search query.
23 |
24 | A TUI is available for interactive search.
25 |
26 | # EXAMPLES
27 |
28 | Find an option and display its details, non-interactively:
29 |
30 | *nixos option \_module.args*
31 |
32 | Find an option and obtain the type using structured JSON output and _jq_:
33 |
34 | *nixos option \_module.args -j | jq .type*
35 |
36 | Find an option using the UI (starting with an initial search):
37 |
38 | *nixos option -i "search.for.option.with.this.name"*
39 |
40 | Find an option in a different flake ref (assume a flake-enabled CLI):
41 |
42 | *nixos option -f "github:MattRStoffel/mixed#nixos-machine" "option.name"*
43 |
44 | # OPTIONS
45 |
46 | *-h*, *--help*
47 | Show the help message for this command.
48 |
49 | *-f*, *--flake* [
50 | Specify an explicit flake *REF* to evaluate options from. Only available
51 | on flake-enabled CLIs.
52 |
53 | If the cache is used to retrieve available options, some options that show
54 | up may actually not be available on the target configuration.
55 |
56 | Use the *--no-cache* flag to fully evaluate the option set for this
57 | configuration to avoid this issue.
58 |
59 | See *nixos-config-env(5)* for the proper flake ref format.
60 |
61 | Default: *$NIXOS_CONFIG*
62 |
63 | *-i*, *--interactive*
64 | Start an interactive TUI for exploring options with a search bar.
65 |
66 | See *nixos-cli-option-ui(1)* for information on how the option TUI works.
67 |
68 | *-j*, *--json*
69 | Output option data as a JSON object.
70 |
71 | Errors will have an "error" key along with "similar_options" with the
72 | list of at max 10 items that have been matched.
73 |
74 | *-s*, *--min-score*
75 | Minimum fuzzy match *SCORE* for filtering results. The bigger the number,
76 | the less search results will appear. However, the results will be more
77 | relevant as they appear if the score is higher.
78 |
79 | Default: *1*
80 |
81 | *-n*, *--no-cache*
82 | Disable usage of the prebuilt options cache.
83 |
84 | Disabling the cache means that the index will need to be built, which takes
85 | time due to Nix evaluation being slow. Use only when the normal option cache
86 | is not working.
87 |
88 | *-v*, *--value-only*
89 | Print only the current value of the selected option.
90 |
91 | Useful for scripts where the option name is needed.
92 |
93 | # ARGUMENTS
94 |
95 | *NAME*
96 | The name of the option to look up. If not provided, interactive mode
97 | is required to explore available options.
98 |
99 | # SEE ALSO
100 |
101 | *nixos-cli-option-ui(1)*
102 |
103 | *nix-instantiate(1)*
104 |
105 | *nix3-eval(1)*
106 |
107 | # AUTHORS
108 |
109 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
110 | details.
111 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-repl.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-REPL(1)
2 |
3 | # NAME
4 |
5 | nixos repl - start a Nix REPL with the current system's configuration loaded
6 |
7 | # SYNOPSIS
8 |
9 | nixos repl [FLAKE-REF] [options]
10 |
11 | # DESCRIPTION
12 |
13 | Launches an interactive Nix expression evaluator (REPL) preloaded with the
14 | NixOS system configuration.
15 |
16 | This command is useful for inspecting the configuration programmatically,
17 | querying attributes, or testing expressions in the context of the active system.
18 |
19 | If a flake-enabled CLI is in use, a flake ref may be supplied to specify the
20 | configuration to load; otherwise, *$NIXOS_CONFIG* will be used and _must_
21 | contain a valid flake ref.
22 |
23 | If not, the environment variable *$NIXOS_CONFIG* will be
24 | used, or the configuration can be passed through setting the *$NIX_PATH*'s
25 | _nixos-config_ attribute properly through *-I* or elsewhere.
26 |
27 | # OPTIONS
28 |
29 | *-h*, *--help*
30 | Show the help message for this command.
31 |
32 | *-I*, *--include*
33 | Specify an additional location to search for Nix expressions. This behaves
34 | like passing *-I* to *nix repl* directly.
35 |
36 | It can be used to add custom search paths, such as
37 | *-I nixpkgs=/path/to/nixpkgs*.
38 |
39 | # ARGUMENTS
40 |
41 | *FLAKE-REF*
42 | Optional flake reference to load attributes from. If the CLI is not
43 | flake-enabled, this argument is ignored.
44 |
45 | Default: *$NIXOS_CONFIG*
46 |
47 | # SEE ALSO
48 |
49 | *nix repl(1)*
50 |
51 | *nixos-cli-env(5)*
52 |
53 | # AUTHORS
54 |
55 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
56 | details.
57 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli-settings.5.scd.template:
--------------------------------------------------------------------------------
1 | NIXOS-CLI-SETTINGS(5)
2 |
3 | # NAME
4 |
5 | nixos-cli-settings - configuration options for nixos-cli
6 |
7 | # DESCRIPTION
8 |
9 | This man page documents available configuration keys for the *nixos-cli* tool.
10 |
11 | Each setting may be configured through a config file, environment variable, or
12 | command-line flag.
13 |
14 | Defaults are noted alongside each option.
15 |
16 | For more information about particular commands, run *nixos --help* or refer to
17 | individual command man pages.
18 |
19 | # FORMAT
20 |
21 | Settings are stored in a configuration file using the TOML format.
22 |
23 | The default location for this file is /etc/nixos-cli/config.toml by default,
24 | but this can be overridden by the *$NIXOS_CLI_CONFIG* environment variable.
25 |
26 | They can also be defined in a NixOS module that will automatically create
27 | this file, if configured through the
28 |
29 | # OPTIONS
30 |
31 | All available settings and their descriptions + default values are located here.
32 |
33 | %s
34 |
35 | # SEE ALSO
36 |
37 | *nixos-cli-env(5)*
38 |
39 | # AUTHORS
40 |
41 | Maintained by the *nixos-cli* team. See the main man page *nixos-cli(1)* for
42 | details.
43 |
--------------------------------------------------------------------------------
/doc/man/nixos-cli.1.scd:
--------------------------------------------------------------------------------
1 | NIXOS-CLI(1)
2 |
3 | # NAME
4 |
5 | nixos-cli - a tool for managing NixOS installations
6 |
7 | # SYNOPSIS
8 |
9 | *nixos* [command] [options]
10 |
11 | # COMMANDS
12 |
13 | *apply*
14 | Build and/or activate a NixOS configuration. This includes evaluating the
15 | NixOS module system, building the system derivation, and switching to the
16 | new generation.
17 |
18 | *enter*
19 | Enter a chroot environment using a provided NixOS installation root. Useful
20 | for debugging, performing repairs, or running commands in the target system
21 | context.
22 |
23 | *features*
24 | Show metadata and features supported by this build of the CLI. This is
25 | mostly useful for diagnosing issues.
26 |
27 | *generation*
28 | List, remove, or inspect generations of the system. Works similarly to
29 | _nix-env --list-generations_ but scoped to the NixOS CLI context.
30 |
31 | *info*
32 | Display information about the currently running generation on the local
33 | system.
34 |
35 | *init*
36 | Initialize a new NixOS configuration in the current directory. This sets up
37 | a default configuration, likely including hardware discovery and system
38 | flake scaffolding.
39 |
40 | *install*
41 | Perform a full system install of NixOS, including setting up the target
42 | drive, copying a configuration, and activating the system.
43 |
44 | *manual*
45 | Open the official NixOS manual in the default browser. This is equivalent to
46 | navigating to but integrated into the CLI.
47 |
48 | *option*
49 | Query the NixOS option system. This allows searching for options, reading
50 | their documentation, defaults, types, and current values.
51 |
52 | *repl*
53 | Start a Nix REPL preloaded with the system configuration and modules. Useful
54 | for experimentation and debugging.
55 |
56 | # OPTIONS
57 |
58 | *--color-always*
59 | Always produce colored output when possible. By default, output may be
60 | auto-detected for TTYs.
61 |
62 | *--config =*
63 | Override configuration settings directly on the command line. Multiple
64 | values can be passed.
65 |
66 | Example: *--config use_nvd=true --config option.prettify=false*
67 |
68 | This only works for simple values such as true/false, string, and number
69 | values. Settings such as *aliases* or *init.extra_attrs* are not settable
70 | through this option due to implementation complexity.
71 |
72 | *-h, --help*
73 | Show the help message for this command.
74 |
75 | *--version*
76 | Display the version of the *nixos-cli* tool.
77 |
78 | # AUTHORS
79 |
80 | Maintained by Varun Narravula . Up-to-date sources can be
81 | found at https://github.com/nix-community/nixos-cli, and bugs reports or patches
82 | can be submitted to GitHub's issue tracker.
83 |
84 | # SEE ALSO
85 |
86 | *nixos-cli-apply(1)*
87 |
88 | *nixos-cli-enter(1)*
89 |
90 | *nixos-cli-features(1)*
91 |
92 | *nixos-cli-generation(1)*
93 |
94 | *nixos-cli-info(1)*
95 |
96 | *nixos-cli-init(1)*
97 |
98 | *nixos-cli-install(1)*
99 |
100 | *nixos-cli-manual(1)*
101 |
102 | *nixos-cli-option(1)*
103 |
104 | *nixos-cli-repl(1)*
105 |
106 | *nixos-cli-settings(5)*
107 |
108 | *nixos-cli-env(5)*
109 |
--------------------------------------------------------------------------------
/doc/src/SUMMARY.md:
--------------------------------------------------------------------------------
1 | [Introduction](introduction.md)
2 |
3 | # Usage
4 |
5 | - [Installation](installation.md)
6 | - [Overview](overview.md)
7 | - [Settings](settings.md)
8 | - [Module](module.md)
9 |
10 | # Community
11 |
12 | - [FAQ](faq.md)
13 | - [Contributing](contributing.md)
14 | - [Roadmap](roadmap.md)
15 |
--------------------------------------------------------------------------------
/doc/src/commands.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nix-community/nixos-cli/4411ef8b00ce2b6247b54949016037b5be0556bd/doc/src/commands.md
--------------------------------------------------------------------------------
/doc/src/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions of all kinds are appreciated! The following are especially
4 | welcome.
5 |
6 | ## Code Contribution
7 |
8 | This is _by far_ the best way to help!
9 |
10 | `nixos-cli` is quite a large tool with a very large scope and many moving parts,
11 | so any efforts to ease the burden around implementation and maintenance is
12 | greatly appreciated. Even so-called "drive-by" contributions or features are
13 | also appreciated, as long as they do not result in excessive maintenance burden
14 | later on.
15 |
16 | Submit contributions through pull requests, or by emailing them personally to
17 | `varun@snare.dev`, if you do not want to use GitHub. Credit will be preserved.
18 |
19 | Please make sure your code is up to standard by running:
20 |
21 | - `gofmt` to format Go code
22 | - `golangci-lint` to catch common issues
23 | - `prettier` to format Markdown files
24 |
25 | All available dependencies are provided in a Nix development shell.
26 |
27 | If your changes modify the CLI or any core behavior, please also update the
28 | relevant `man` pages or documentation in `doc/`. This includes changes to the
29 | following things:
30 |
31 | - CLI commands/options
32 | - Settings
33 | - NixOS module options
34 |
35 | ## Bug Reports
36 |
37 | Testing every feature edge-case is hard—especially before full releases.
38 |
39 | If you're a brave soul, use the main branch instead of a release version, and
40 | file bug reports by
41 | [opening a new issue](https://github.com/nix-community/nixos-cli/issues) with
42 | the **Bug Report** template. In the bug report, provide:
43 |
44 | - A clear description of the problem
45 | - **IMPORTANT**: What was _expected_ vs. what actually _happened_
46 | - Steps to reproduce the issue
47 | - Your environment (run `nixos features`)
48 | - Any relevant logs, error messages, or images
49 |
50 | Clear reports will assist in faster bug fixes!
51 |
52 | ## Improving Documentation
53 |
54 | Nix documentation is notoriously patchy — so help here is _especially_ welcome.
55 |
56 | As such, documentation quality is of utmost importance. `nixos-cli` should be a
57 | tool that is both easy to use and powerful in functionality; however, as
58 | powerful as it can be, who cares if that power isn't discoverable?
59 |
60 | Documentation lives in two places:
61 |
62 | - Markdown files for this website, generated using
63 | [`mdbook`](https://rust-lang.github.io/mdBook/)
64 | - Manual pages (`man` pages), generated using
65 | [`scdoc`](https://sr.ht/~sircmpwn/scdoc/)
66 |
67 | Refer to the code contribution guidelines when submitting documentation
68 | improvements, or file an issue if the documentation issues are substantial.
69 |
70 | ## Feature Suggestions
71 |
72 | Have an idea for improving NixOS tooling here? Start a discussion or open an
73 | issue!
74 |
75 | Discourse around how this can be done is always productive.
76 |
77 | The vision is to make this a standard NixOS tool, so all ideas that align with
78 | that scope are welcome. If there’s a new command or sub-tool you’d like to see,
79 | open a GitHub issue or reach out on Matrix. However, try to keep it within scope
80 | of the NixOS project, though.
81 |
82 | ❌ Features like `home-manager` or `nix-darwin` integration will not be
83 | considered as first-class features. Sorry in advance.
84 |
85 | ## Community Conduct
86 |
87 | All contributors must follow a friendly, respectful code of conduct.
88 |
89 | The TL;DR? **Don't be a dick.**
90 |
91 | Disagreement is fine, but harassment, rudeness, or discrimination are not
92 | tolerated in any spaces.
93 |
--------------------------------------------------------------------------------
/doc/src/faq.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | ## Do you intend on making this an official NixOS application?
4 |
5 | Yes! I will be writing an RFC to see if people are interested in this.
6 |
7 | That's my ultimate goal, anyway.
8 |
9 | ## What about [`nh`](https://github.com/nix-community/nh)? Isn't that better since it supports more OSes?
10 |
11 | `nh` is a more popular application in this realm, perhaps because it looked
12 | prettier due to the earlier `nix-output-monitor` and `nvd` integration, and is
13 | significantly older than `nixos-cli`.
14 |
15 | However, I prefer to keep the focus on NixOS here, while `nh` tries to be a
16 | unified `rebuild` + `switch` manager for multiple OSes. That's the difference.
17 |
18 | `nixos-cli` also has more features than `nh` for NixOS-based machines, so that's
19 | a plus.
20 |
21 | ## What about `home-manager` and `nix-darwin`? Will you support those systems?
22 |
23 | They are fundamentally separate projects with roughly similar surfaces, so no. I
24 | am a heavy user of both projects, though, so I may write my own `darwin` and
25 | `hm` CLIs that roughly mirror this.
26 |
27 | Think about this:
28 |
29 | - `home-manager` has to work in the user context, while NixOS works in the
30 | system one.
31 | - `nix-darwin` doesn't interact with boot scripts, while NixOS does.
32 |
33 | Among a slew of other differences. The `rebuild` + `switch` workflow may be the
34 | same, but the options are different, and I'm lazy. So no.
35 |
36 | ## Can the option search work with other sources?
37 |
38 | It's theoretically possible, as long as the modules can be evaluated with
39 | `lib.evalModules`. As such, `home-manager`, `nix-darwin`, and even `flake-parts`
40 | are possible to do!
41 |
42 | However, this tends to significantly increase evaluation time, and will depend
43 | on the system to eval. I plan to break out the option search UI into a separate
44 | project that can be more generalized, and add it back to this one as a plugin of
45 | sorts.
46 |
47 | ## More questions?
48 |
49 | File an issue! Perhaps it's important enough to add to this FAQ as well.
50 |
--------------------------------------------------------------------------------
/doc/src/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | `nixos-cli` is split into two separate executables. This is very intentional for
4 | a number of reasons.
5 |
6 | 1. The majority of NixOS users use either flakes or legacy-style Nix, without
7 | mixing the two.
8 | 2. While the majority of logic is shared between the two styles of
9 | configuration, the command-line interface should not be forced to deal with
10 | the differences, for the sake of clarity.
11 | 3. If users want to mix styles, they should do so intentionally. This
12 | distinction is reflected in the CLI binaries themselves—not hidden in command
13 | behavior.
14 |
15 | The flake-style configuration is the default. Nix flakes have been available for
16 | several years; although still technically experimental, they are widely adopted
17 | and considered stable in practice, particularly in forks like
18 | [Lix](https://lix.systems). Legacy configurations are actively supported
19 | regardless of this status, though.
20 |
21 | NixOS has quite a large ecosystem of tools, and can be quite the moving target
22 | in terms of features, so `nixos-unstable` and the current stable release are the
23 | only actively supported releases.
24 |
25 | ## Adding To Configuration
26 |
27 | Use the following sections depending on whether or not your systems are
28 | configured with flakes or legacy-style configurations.
29 |
30 | Available configuration settings for `nixos-cli` are defined in the more
31 | detailed [settings](./usage/settings.md) section, and are specified in Nix
32 | attribute set format here. Internally, they are converted to TOML.
33 |
34 | ### Flakes
35 |
36 | `nixos-cli` is provided as a flake input. Add this and the exported NixOS module
37 | to the system configuration.
38 |
39 | ```nix
40 | {
41 | inputs.nixos-cli.url = "github:nix-community/nixos-cli";
42 |
43 | outputs = { nixpkgs, nixos-cli, ... }: {
44 | nixosConfigurations.system-name = nixpkgs.lib.nixosSystem {
45 | modules = [
46 | nixos-cli.nixosModules.nixos-cli
47 | # ...
48 | ];
49 | };
50 | };
51 | }
52 | ```
53 |
54 | Then, enable the module.
55 |
56 | ```nix
57 | { config, pkgs, ... }:
58 |
59 | {
60 | services.nixos-cli = {
61 | enable = true;
62 | config = {
63 | # Whatever settings desired.
64 | }
65 | };
66 | }
67 | ```
68 |
69 | The default package is flake-enabled, so the `services.nixos-cli.package` option
70 | does not need to be specified.
71 |
72 | ### Legacy
73 |
74 | To use the NixOS module in legacy mode, import the `default.nix` provided in
75 | this repository. An example is provided below with `builtins.fetchTarball`:
76 |
77 | ```nix
78 | { config, system, pkgs, ...}:
79 |
80 | let
81 | # In pure evaluation mode, always use a full Git commit hash instead of a branch name.
82 | nixos-cli-url = "github:nix-community/nixos-cli/archive/GITREVORBRANCHDEADBEEFDEADBEEF0000.tar.gz";
83 | nixos-cli = import "${builtins.fetchTarball nixos-cli-url}" {inherit pkgs;};
84 | in {
85 | imports = [
86 | nixos-cli.module
87 | ];
88 |
89 | services.nixos-cli = {
90 | enable = true;
91 | package = nixos-cli.nixosLegacy;
92 | config = {
93 | # Other configuration for nixos-cli
94 | };
95 | };
96 |
97 | # ... rest of config
98 | }
99 | ```
100 |
101 | NOTE: Use the `nixosLegacy` package. Specifying the `services.nixos-cli.package`
102 | option is required for legacy configurations, due to the fact that the default
103 | package is for flake configurations only. If there is a reliable way to detect
104 | if a configuration is flake-enabled, please file an
105 | [issue](https://github.com/nix-community/nixos-cli/issues/new/choose) so that
106 | this requirement can be removed.
107 |
108 | ## Cache
109 |
110 | There is a Cachix cache available. Add the following to your NixOS configuration
111 | to avoid lengthy rebuilds and fetching extra build-time dependencies:
112 |
113 | ```nix
114 | {
115 | nix.settings = {
116 | substituters = [ "https://watersucks.cachix.org" ];
117 | trusted-public-keys = [
118 | "watersucks.cachix.org-1:6gadPC5R8iLWQ3EUtfu3GFrVY7X6I4Fwz/ihW25Jbv8="
119 | ];
120 | };
121 | }
122 | ```
123 |
124 | Or if using the Cachix CLI outside a NixOS environment:
125 |
126 | ```sh
127 | $ cachix use watersucks
128 | ```
129 |
130 | There are rare cases in which you want to automatically configure a cache when
131 | using flakes, such as when installing NixOS configurations using this tool. The
132 | following configuration in the `flake.nix` can help with this:
133 |
134 | ```nix
135 | {
136 | nixConfig = {
137 | extra-substituters = [ "https://watersucks.cachix.org" ];
138 | extra-trusted-public-keys = [
139 | "watersucks.cachix.org-1:6gadPC5R8iLWQ3EUtfu3GFrVY7X6I4Fwz/ihW25Jbv8="
140 | ];
141 | };
142 |
143 | inputs = {}; # Whatever you normally have here
144 | outputs = inputs: {}; # Whatever you normally have here
145 | }
146 | ```
147 |
148 | ⚠️ Beware, though: this is a relatively undocumented feature—use with caution.
149 |
150 | ## Running Using Nix Shells
151 |
152 | Sometimes, you may not want to add it to your configuration, and instead run
153 | `nixos-cli` on an ad-hoc basis.
154 |
155 | This is the preferred way to use `nixos-cli` when running `nixos init` or
156 | `nixos install` on a live NixOS USB for installation.
157 |
158 | Use `nix develop` (flake-enabled package by default):
159 |
160 | ```
161 | $ nix shell github:nix-community/nixos-cli
162 | ```
163 |
164 | Alternative using legacy-style `nix-shell` and the `nixosLegacy` package:
165 |
166 | ```sh
167 | $ nix-shell -E 'with import (fetchTarball "https://github.com/nix-community/nixos-cli/archive/refs/heads/main.tar.gz") {}; nixosLegacy'
168 | ```
169 |
170 | ## Rebuild
171 |
172 | After adding the next sections to your configuration, rebuild your configuration
173 | once, and then the `nixos` command should be available. Verify by running
174 | `nixos features`:
175 |
176 | ```sh
177 | # Example output of `nixos features`
178 | $ nixos features
179 | nixos 0.13.0-dev
180 | git rev: 53beba5f09042ab8361708a5e0196098d642ba5b
181 | go version: go1.24.1
182 | nix version: nix (Nix) 2.28.2
183 |
184 | Compilation Options
185 | -------------------
186 | flake :: true
187 | nixpkgs_version :: 24.11
188 | ```
189 |
190 | Nice! `nixos-cli` is now ready for usage.
191 |
--------------------------------------------------------------------------------
/doc/src/introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction - `nixos-cli`
2 |
3 | `nixos-cli` is a robust, cohesive, drop-in replacement for NixOS tooling such as
4 | `nixos-rebuild`, among many other tools.
5 |
6 | ## Why?
7 |
8 | NixOS tooling today is fragmented across large, aging shell and Perl scripts
9 | that are difficult to maintain or extend. Prolific examples include:
10 |
11 | - [`nixos-rebuild.sh`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/linux/nixos-rebuild/nixos-rebuild.sh)
12 | - [`switch-to-configuration.pl`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/system/activation/switch-to-configuration.pl)
13 |
14 | These tools contain deep functionality, but much of it is hidden, hard to
15 | modify, or locked behind poor ergonomics.
16 |
17 | `nixos-cli` aims to modernize this experience by:
18 |
19 | - Replacing and improving existing tooling
20 | - Providing a consistent interface across all commands
21 | - Making functionality more accessible and extensible
22 | - Offering a clean, discoverable CLI experience for both users and developers
23 |
24 | In summary, this tool has one goal: to create a modular NixOS CLI that mirrors
25 | or enhances the functionality of all current NixOS tooling in `nixpkgs`, adds on
26 | to it if needed, and eventually **come to replace it entirely**.
27 |
28 | Yes, this is already being done somewhat by `switch-to-configuration-ng` and
29 | `nixos-rebuild-ng`. However, `nixos-cli` strives to achieve further goals,
30 | including (but not limited to the following)
31 |
32 | - Enhanced usability (and looking nice! Who doesn't love eye candy?)
33 | - Deeper integration with NixOS internals
34 | - Creating a self-contained NixOS manager binary that includes routine scripts
35 | such as `switch-to-configuration` activation functionality
36 | - Plugins for further NixOS tooling to be developed out-of-tree
37 |
38 | ## Key Features
39 |
40 | - Drop-in replacements for common NixOS tools (with better names!)
41 | - An integrated NixOS option search UI
42 | - An improved generation manager, with an additional UI (more fine-tuned than
43 | `nix-collect-garbage -d`)
44 |
45 | Check out the [overview](./overview.md) page for more information about key
46 | features.
47 |
48 | More features are planned; see the [roadmap](roadmap.md) for more information.
49 |
50 | ## Status
51 |
52 | This tool is under **active development**, but is **not yet stable**.
53 | Until a 1.0 release, the CLI interface and configuration may change without
54 | notice.
55 |
56 | Watch the [Releases](https://github.com/nix-community/nixos-cli/releases) page
57 | for:
58 |
59 | - Breaking changes
60 | - Feature updates
61 | - Bug fixes
62 |
63 | Core contributors:
64 |
65 | - [`@water-sucks`](https://github.com/water-sucks)
66 |
67 | Contributions, testing, and bug reports/general feedback are highly encouraged,
68 | since there are few people working on this project actively.
69 |
70 | ## Talk!
71 |
72 | Join the Matrix room at
73 | [#nixos-cli:matrix.org](https://matrix.to/#/#nixos-cli:matrix.org)! It's open
74 | for chatting about NixOS in general, and for making it a better experience for
75 | all that involved.
76 |
--------------------------------------------------------------------------------
/doc/src/module.md:
--------------------------------------------------------------------------------
1 | # Module
2 |
3 | These are the available NixOS module options for `nixos-cli`. This is the
4 | preferred way to configure things like settings, and to add the CLI itself to
5 | the `$PATH`.
6 |
7 | {{ #include generated-module.md }}
8 |
--------------------------------------------------------------------------------
/doc/src/roadmap.md:
--------------------------------------------------------------------------------
1 | # Roadmap
2 |
3 | - ❌ Remote application of configurations
4 | - ❌ Remote installation of NixOS configurations (a la
5 | [`nixos-anywhere`](https;//github.com/numtide/nixos-anywhere))
6 | - ❌ Ability to remotely manage generations
7 | - ❌ Smart container management (a la `nixos-container`)
8 | - ❌ Explicit ability to deploy systems from non-NixOS operating systems
9 | - ❌ **Drafting an RFC** to make `nixos-cli` the default NixOS management tool
10 |
11 | See the [Issues](https://github.com/nix-community/nixos-cli/issues) section in
12 | the GitHub repository for a list of other issues and/or features to be
13 | implemented or make your own requests! All requests are welcome.
14 |
15 |
--------------------------------------------------------------------------------
/doc/src/settings.md:
--------------------------------------------------------------------------------
1 | # Settings
2 |
3 | Settings are stored in `/etc/nixos-cli/config.toml`, and are stored in
4 | [`TOML`](https://toml.io) format.
5 |
6 | If preferred, this can be overridden by an environment variable
7 | `NIXOS_CLI_CONFIG` at runtime. This is useful for testing configuration files.
8 |
9 | Additionally, some configuration values can be overridden on the command-line
10 | with the `--config` flag.
11 |
12 | Example invocation:
13 |
14 | ```sh
15 | $ nixos --config apply.imply_impure_with_tag=false apply
16 | ```
17 |
18 | The preferred way to create this settings file is through the provided Nix
19 | module that generates the TOML using the `services.nixos-cli.config` option.
20 | Refer to the [module documentation](./module.md) for other available options.
21 |
22 | ## Available Settings
23 |
24 | These are the available settings for `nixos-cli` and their default values.
25 |
26 | {{ #include generated-settings.md }}
27 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-compat": {
4 | "flake": false,
5 | "locked": {
6 | "lastModified": 1747046372,
7 | "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
8 | "owner": "edolstra",
9 | "repo": "flake-compat",
10 | "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
11 | "type": "github"
12 | },
13 | "original": {
14 | "owner": "edolstra",
15 | "repo": "flake-compat",
16 | "type": "github"
17 | }
18 | },
19 | "flake-utils": {
20 | "inputs": {
21 | "systems": "systems"
22 | },
23 | "locked": {
24 | "lastModified": 1731533236,
25 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
26 | "owner": "numtide",
27 | "repo": "flake-utils",
28 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
29 | "type": "github"
30 | },
31 | "original": {
32 | "owner": "numtide",
33 | "repo": "flake-utils",
34 | "type": "github"
35 | }
36 | },
37 | "nix-options-doc": {
38 | "inputs": {
39 | "flake-utils": "flake-utils",
40 | "nixpkgs": "nixpkgs",
41 | "rust-overlay": "rust-overlay"
42 | },
43 | "locked": {
44 | "lastModified": 1742115705,
45 | "narHash": "sha256-RfXwJPWBoWswIU68+y/XZfTWtFHd/fK14bKvOlRmfPo=",
46 | "owner": "Thunderbottom",
47 | "repo": "nix-options-doc",
48 | "rev": "2caa4b5756a8666d65d70122f413e295f56886e7",
49 | "type": "github"
50 | },
51 | "original": {
52 | "owner": "Thunderbottom",
53 | "ref": "v0.2.0",
54 | "repo": "nix-options-doc",
55 | "type": "github"
56 | }
57 | },
58 | "nixpkgs": {
59 | "locked": {
60 | "lastModified": 1740695751,
61 | "narHash": "sha256-D+R+kFxy1KsheiIzkkx/6L63wEHBYX21OIwlFV8JvDs=",
62 | "owner": "nixos",
63 | "repo": "nixpkgs",
64 | "rev": "6313551cd05425cd5b3e63fe47dbc324eabb15e4",
65 | "type": "github"
66 | },
67 | "original": {
68 | "owner": "nixos",
69 | "ref": "nixos-unstable",
70 | "repo": "nixpkgs",
71 | "type": "github"
72 | }
73 | },
74 | "nixpkgs_2": {
75 | "locked": {
76 | "lastModified": 1748506378,
77 | "narHash": "sha256-oS0Gxh63Df8b8r04lqEYDDLKhHIrVr9/JLOn2bn8JaI=",
78 | "owner": "NixOS",
79 | "repo": "nixpkgs",
80 | "rev": "3866ad91cfc172f08a6839def503d8fc2923c603",
81 | "type": "github"
82 | },
83 | "original": {
84 | "owner": "NixOS",
85 | "ref": "nixpkgs-unstable",
86 | "repo": "nixpkgs",
87 | "type": "github"
88 | }
89 | },
90 | "root": {
91 | "inputs": {
92 | "flake-compat": "flake-compat",
93 | "nix-options-doc": "nix-options-doc",
94 | "nixpkgs": "nixpkgs_2"
95 | }
96 | },
97 | "rust-overlay": {
98 | "inputs": {
99 | "nixpkgs": [
100 | "nix-options-doc",
101 | "nixpkgs"
102 | ]
103 | },
104 | "locked": {
105 | "lastModified": 1740796337,
106 | "narHash": "sha256-FuoXrXZPoJEZQ3PF7t85tEpfBVID9JQIOnVKMNfTAb0=",
107 | "owner": "oxalica",
108 | "repo": "rust-overlay",
109 | "rev": "bbac9527bc6b28b6330b13043d0e76eac11720dc",
110 | "type": "github"
111 | },
112 | "original": {
113 | "owner": "oxalica",
114 | "repo": "rust-overlay",
115 | "type": "github"
116 | }
117 | },
118 | "systems": {
119 | "locked": {
120 | "lastModified": 1681028828,
121 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
122 | "owner": "nix-systems",
123 | "repo": "default",
124 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
125 | "type": "github"
126 | },
127 | "original": {
128 | "owner": "nix-systems",
129 | "repo": "default",
130 | "type": "github"
131 | }
132 | }
133 | },
134 | "root": "root",
135 | "version": 7
136 | }
137 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "A unified NixOS tooling replacement for nixos-* utilities";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
6 |
7 | flake-compat = {
8 | url = "github:edolstra/flake-compat";
9 | flake = false;
10 | };
11 |
12 | nix-options-doc.url = "github:Thunderbottom/nix-options-doc/v0.2.0";
13 | };
14 |
15 | outputs = {
16 | self,
17 | nixpkgs,
18 | ...
19 | } @ inputs: let
20 | inherit (nixpkgs) lib;
21 | eachSystem = lib.genAttrs lib.systems.flakeExposed;
22 | pkgsFor = system: nixpkgs.legacyPackages.${system};
23 | in {
24 | packages = eachSystem (system: let
25 | pkgs = pkgsFor system;
26 | inherit (pkgs) callPackage;
27 | in {
28 | default = self.packages.${pkgs.system}.nixos;
29 |
30 | nixos = callPackage ./package.nix {
31 | revision = self.rev or self.dirtyRev or "unknown";
32 | };
33 | nixosLegacy = self.packages.${pkgs.system}.nixos.override {flake = false;};
34 | });
35 |
36 | devShells = eachSystem (system: let
37 | pkgs = pkgsFor system;
38 | inherit (pkgs) go golangci-lint mkShell mdbook scdoc;
39 | inherit (pkgs.nodePackages) prettier;
40 |
41 | nix-options-doc = inputs.nix-options-doc.packages.${system}.default;
42 | in {
43 | default = mkShell {
44 | name = "nixos-shell";
45 | nativeBuildInputs = [
46 | go
47 | golangci-lint
48 |
49 | mdbook
50 | prettier
51 | scdoc
52 | nix-options-doc
53 | ];
54 | };
55 | });
56 |
57 | nixosModules.nixos-cli = lib.modules.importApply ./module.nix self;
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/nix-community/nixos-cli
2 |
3 | go 1.24.2
4 |
5 | toolchain go1.24.3
6 |
7 | require (
8 | github.com/charmbracelet/bubbles v0.21.0
9 | github.com/charmbracelet/bubbletea v1.3.5
10 | github.com/charmbracelet/glamour v0.10.0
11 | github.com/djherbis/times v1.6.0
12 | github.com/fatih/color v1.18.0
13 | github.com/go-git/go-git/v5 v5.13.1
14 | github.com/knadh/koanf/parsers/toml/v2 v2.2.0
15 | github.com/knadh/koanf/providers/file v1.2.0
16 | github.com/knadh/koanf/v2 v2.2.0
17 | github.com/olekukonko/tablewriter v0.0.5
18 | github.com/sahilm/fuzzy v0.1.1
19 | github.com/spf13/cobra v1.9.1
20 | github.com/spf13/pflag v1.0.6
21 | github.com/yarlson/pin v0.9.1
22 | golang.org/x/term v0.32.0
23 | )
24 |
25 | require (
26 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
27 | github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
28 | github.com/charmbracelet/x/ansi v0.9.2 // indirect
29 | github.com/charmbracelet/x/term v0.2.1 // indirect
30 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
31 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
32 | github.com/mattn/go-localereader v0.0.1 // indirect
33 | github.com/mattn/go-runewidth v0.0.16 // indirect
34 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
35 | github.com/muesli/cancelreader v0.2.2 // indirect
36 | github.com/muesli/termenv v0.16.0
37 | github.com/rivo/uniseg v0.4.7 // indirect
38 | golang.org/x/text v0.25.0 // indirect
39 | )
40 |
41 | require (
42 | dario.cat/mergo v1.0.1 // indirect
43 | github.com/Microsoft/go-winio v0.6.2 // indirect
44 | github.com/ProtonMail/go-crypto v1.1.5 // indirect
45 | github.com/alecthomas/chroma/v2 v2.18.0 // indirect
46 | github.com/atotto/clipboard v0.1.4 // indirect
47 | github.com/aymerick/douceur v0.2.0 // indirect
48 | github.com/charmbracelet/colorprofile v0.3.1 // indirect
49 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
50 | github.com/charmbracelet/x/exp/slice v0.0.0-20250528180458-2d5d6cb84620 // indirect
51 | github.com/cloudflare/circl v1.5.0 // indirect
52 | github.com/cyphar/filepath-securejoin v0.4.0 // indirect
53 | github.com/dlclark/regexp2 v1.11.5 // indirect
54 | github.com/emirpasic/gods v1.18.1 // indirect
55 | github.com/fsnotify/fsnotify v1.9.0 // indirect
56 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
57 | github.com/go-git/go-billy/v5 v5.6.2 // indirect
58 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
59 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
60 | github.com/gorilla/css v1.0.1 // indirect
61 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
62 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
63 | github.com/kevinburke/ssh_config v1.2.0 // indirect
64 | github.com/knadh/koanf/maps v0.1.2 // indirect
65 | github.com/mattn/go-colorable v0.1.14 // indirect
66 | github.com/mattn/go-isatty v0.0.20 // indirect
67 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect
68 | github.com/mitchellh/copystructure v1.2.0 // indirect
69 | github.com/mitchellh/reflectwalk v1.0.2 // indirect
70 | github.com/mmcloughlin/avo v0.6.0 // indirect
71 | github.com/muesli/reflow v0.3.0 // indirect
72 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect
73 | github.com/pjbgf/sha1cd v0.3.1 // indirect
74 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
75 | github.com/skeema/knownhosts v1.3.0 // indirect
76 | github.com/water-sucks/optnix v0.1.2 // indirect
77 | github.com/xanzy/ssh-agent v0.3.3 // indirect
78 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
79 | github.com/yuin/goldmark v1.7.12 // indirect
80 | github.com/yuin/goldmark-emoji v1.0.6 // indirect
81 | golang.org/x/crypto v0.38.0 // indirect
82 | golang.org/x/mod v0.22.0 // indirect
83 | golang.org/x/net v0.40.0 // indirect
84 | golang.org/x/sync v0.14.0 // indirect
85 | golang.org/x/sys v0.33.0 // indirect
86 | golang.org/x/tools v0.29.0 // indirect
87 | gopkg.in/warnings.v0 v0.1.2 // indirect
88 | )
89 |
--------------------------------------------------------------------------------
/internal/activation/activation.go:
--------------------------------------------------------------------------------
1 | package activation
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "regexp"
8 | "strconv"
9 |
10 | "github.com/nix-community/nixos-cli/internal/constants"
11 | "github.com/nix-community/nixos-cli/internal/generation"
12 | "github.com/nix-community/nixos-cli/internal/settings"
13 | "github.com/nix-community/nixos-cli/internal/system"
14 | )
15 |
16 | // Parse the generation's `nixos-cli` configuration to find the default specialisation
17 | // for that generation.
18 | func FindDefaultSpecialisationFromConfig(generationDirname string) (string, error) {
19 | generationCfgFilename := filepath.Join(generationDirname, constants.DefaultConfigLocation)
20 | generationCfg, err := settings.ParseSettings(generationCfgFilename)
21 | if err != nil {
22 | return "", err
23 | }
24 |
25 | return generationCfg.Apply.DefaultSpecialisation, nil
26 | }
27 |
28 | // Make sure a specialisation exists in a given generation and can be activated by
29 | // checking for the presence of the switch-to-configuration script.
30 | func VerifySpecialisationExists(generationDirname string, specialisation string) bool {
31 | if specialisation == "" {
32 | // The base config always exists.
33 | return true
34 | }
35 |
36 | specialisationStcFilename := filepath.Join(generationDirname, "specialisation", specialisation, "bin", "switch-to-configuration")
37 | if _, err := os.Stat(specialisationStcFilename); err != nil {
38 | return false
39 | }
40 |
41 | return true
42 | }
43 |
44 | func EnsureSystemProfileDirectoryExists() error {
45 | // The system profile directory sometimes doesn't exist,
46 | // and does need to be manually created if this is the case.
47 | // This kinda sucks, since it requires root execution, but
48 | // there's not really a better way to ensure that this
49 | // profile's directory exists.
50 |
51 | err := os.MkdirAll(constants.NixSystemProfileDirectory, 0o755)
52 | if err != nil {
53 | if err != os.ErrExist {
54 | return fmt.Errorf("failed to create nix system profile directory: %w", err)
55 | }
56 | }
57 |
58 | return nil
59 | }
60 |
61 | func AddNewNixProfile(s system.CommandRunner, profile string, closure string, verbose bool) error {
62 | if profile != "system" {
63 | err := EnsureSystemProfileDirectoryExists()
64 | if err != nil {
65 | return err
66 | }
67 | }
68 |
69 | profileDirectory := generation.GetProfileDirectoryFromName(profile)
70 |
71 | argv := []string{"nix-env", "--profile", profileDirectory, "--set", closure}
72 |
73 | if verbose {
74 | s.Logger().CmdArray(argv)
75 | }
76 |
77 | cmd := system.NewCommand(argv[0], argv[1:]...)
78 |
79 | _, err := s.Run(cmd)
80 |
81 | return err
82 | }
83 |
84 | func SetNixProfileGeneration(s system.CommandRunner, profile string, genNumber uint64, verbose bool) error {
85 | if profile != "system" {
86 | err := EnsureSystemProfileDirectoryExists()
87 | if err != nil {
88 | return err
89 | }
90 | }
91 |
92 | profileDirectory := generation.GetProfileDirectoryFromName(profile)
93 |
94 | argv := []string{"nix-env", "--profile", profileDirectory, "--switch-generation", fmt.Sprintf("%d", genNumber)}
95 |
96 | if verbose {
97 | s.Logger().CmdArray(argv)
98 | }
99 |
100 | cmd := system.NewCommand(argv[0], argv[1:]...)
101 |
102 | _, err := s.Run(cmd)
103 |
104 | return err
105 | }
106 |
107 | func GetCurrentGenerationNumber(profile string) (uint64, error) {
108 | genLinkRegex, err := regexp.Compile(fmt.Sprintf(generation.GenerationLinkTemplateRegex, profile))
109 | if err != nil {
110 | return 0, fmt.Errorf("failed to compile generation regex: %w", err)
111 | }
112 |
113 | profileDirectory := generation.GetProfileDirectoryFromName(profile)
114 | currentGenerationLink, err := os.Readlink(profileDirectory)
115 | if err != nil {
116 | return 0, fmt.Errorf("unable to determine current generation: %v", err)
117 | }
118 |
119 | if matches := genLinkRegex.FindStringSubmatch(currentGenerationLink); len(matches) > 0 {
120 | genNumber, err := strconv.ParseInt(matches[1], 10, 64)
121 | if err != nil {
122 | return 0, fmt.Errorf("failed to parse generation number %v for %v", matches[1], currentGenerationLink)
123 | }
124 |
125 | return uint64(genNumber), nil
126 | } else {
127 | panic("current link format does not match 'profile-generation-link' format")
128 | }
129 | }
130 |
131 | type SwitchToConfigurationAction int
132 |
133 | const (
134 | SwitchToConfigurationActionSwitch = iota
135 | SwitchToConfigurationActionBoot
136 | SwitchToConfigurationActionTest
137 | SwitchToConfigurationActionDryActivate
138 | )
139 |
140 | func (c SwitchToConfigurationAction) String() string {
141 | switch c {
142 | case SwitchToConfigurationActionSwitch:
143 | return "switch"
144 | case SwitchToConfigurationActionBoot:
145 | return "boot"
146 | case SwitchToConfigurationActionTest:
147 | return "test"
148 | case SwitchToConfigurationActionDryActivate:
149 | return "dry-activate"
150 | default:
151 | panic("unknown switch to configuration action type")
152 | }
153 | }
154 |
155 | type SwitchToConfigurationOptions struct {
156 | InstallBootloader bool
157 | Verbose bool
158 | Specialisation string
159 | }
160 |
161 | func SwitchToConfiguration(s system.CommandRunner, generationLocation string, action SwitchToConfigurationAction, opts *SwitchToConfigurationOptions) error {
162 | var commandPath string
163 | if opts.Specialisation != "" {
164 | commandPath = filepath.Join(generationLocation, "specialisation", opts.Specialisation, "bin", "switch-to-configuration")
165 | } else {
166 | commandPath = filepath.Join(generationLocation, "bin", "switch-to-configuration")
167 | }
168 |
169 | argv := []string{commandPath, action.String()}
170 |
171 | if opts.Verbose {
172 | s.Logger().CmdArray(argv)
173 | }
174 |
175 | cmd := system.NewCommand(argv[0], argv[1:]...)
176 | if opts.InstallBootloader {
177 | cmd.SetEnv("NIXOS_INSTALL_BOOTLOADER", "1")
178 | }
179 |
180 | _, err := s.Run(cmd)
181 |
182 | return err
183 | }
184 |
--------------------------------------------------------------------------------
/internal/build/vars.go:
--------------------------------------------------------------------------------
1 | package buildOpts
2 |
3 | // Do not change these. These are always going to be set
4 | // at compile-time.
5 |
6 | var (
7 | Version string = "unknown"
8 | GitRevision string = "unknown"
9 | Flake string = "true"
10 | NixpkgsVersion string = ""
11 | )
12 |
13 | func boolCheck(varName string, value string) {
14 | if value != "true" && value != "false" {
15 | panic("Compile-time variable internal.build." + varName + " is not a value of either 'true' or 'false'; this application was compiled incorrectly")
16 | }
17 | }
18 |
19 | func init() {
20 | boolCheck("Flake", Flake)
21 | }
22 |
--------------------------------------------------------------------------------
/internal/cmd/errors/errors.go:
--------------------------------------------------------------------------------
1 | package errors
2 |
3 | type ArgError struct {
4 | Message string
5 | Hint string
6 | }
7 |
8 | func (e ArgError) Error() string {
9 | return e.Message
10 | }
11 |
--------------------------------------------------------------------------------
/internal/cmd/nixopts/convert.go:
--------------------------------------------------------------------------------
1 | package nixopts
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "sort"
7 |
8 | "github.com/spf13/pflag"
9 | )
10 |
11 | var availableOptions = map[string]string{
12 | "Quiet": "quiet",
13 | "PrintBuildLogs": "print-build-logs",
14 | "NoBuildOutput": "fallback",
15 | "ShowTrace": "show-trace",
16 | "KeepGoing": "keep-going",
17 | "KeepFailed": "keep-failed",
18 | "Fallback": "fallback",
19 | "Refresh": "refresh",
20 | "Repair": "repair",
21 | "Impure": "impure",
22 | "Offline": "offline",
23 | "NoNet": "no-net",
24 | "MaxJobs": "max-jobs",
25 | "Cores": "cores",
26 | "LogFormat": "log-format",
27 | "Options": "option",
28 | "Builders": "builders",
29 | "RecreateLockFile": "recreate-lock-file",
30 | "NoUpdateLockFile": "no-update-lock-file",
31 | "NoWriteLockFile": "no-write-lock-file",
32 | "NoUseRegistries": "no-use-registries",
33 | "CommitLockFile": "commit-lock-file",
34 | "UpdateInputs": "update-inputs",
35 | "OverrideInputs": "override-input",
36 | "Includes": "include",
37 | }
38 |
39 | func getNixFlag(name string) string {
40 | if option, ok := availableOptions[name]; ok {
41 | return option
42 | }
43 |
44 | panic("unknown option '" + name + "' when trying to convert to nix options struct")
45 | }
46 |
47 | func NixOptionsToArgsList(flags *pflag.FlagSet, options any) []string {
48 | val := reflect.ValueOf(options)
49 | typ := reflect.TypeOf(options)
50 |
51 | if val.Kind() == reflect.Ptr {
52 | val = val.Elem()
53 | typ = typ.Elem()
54 | }
55 |
56 | args := make([]string, 0)
57 |
58 | for i := 0; i < val.NumField(); i++ {
59 | field := val.Field(i)
60 | fieldType := typ.Field(i)
61 | fieldName := getNixFlag(fieldType.Name)
62 |
63 | if !flags.Changed(fieldName) {
64 | continue
65 | }
66 |
67 | optionArg := fmt.Sprintf("--%s", fieldName)
68 |
69 | switch field.Kind() {
70 | case reflect.Bool:
71 | if field.Bool() {
72 | args = append(args, optionArg)
73 | }
74 | case reflect.Int:
75 | args = append(args, optionArg, fmt.Sprintf("%d", field.Int()))
76 | case reflect.String:
77 | if field.String() != "" {
78 | args = append(args, optionArg, field.String())
79 | }
80 | case reflect.Slice:
81 | if field.Len() > 0 {
82 | for j := 0; j < field.Len(); j++ {
83 | args = append(args, optionArg, field.Index(j).String())
84 | }
85 | }
86 | case reflect.Map:
87 | keys := field.MapKeys()
88 |
89 | sort.Slice(keys, func(i, j int) bool {
90 | return keys[i].String() < keys[j].String()
91 | })
92 |
93 | for _, key := range keys {
94 | value := field.MapIndex(key)
95 | args = append(args, optionArg, key.String(), value.String())
96 | }
97 | default:
98 | panic("unsupported field type " + field.Kind().String() + " for field '" + fieldName + "'")
99 | }
100 | }
101 |
102 | return args
103 | }
104 |
--------------------------------------------------------------------------------
/internal/cmd/nixopts/convert_test.go:
--------------------------------------------------------------------------------
1 | package nixopts_test
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/nix-community/nixos-cli/internal/cmd/nixopts"
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | type nixOptions struct {
12 | Quiet bool
13 | PrintBuildLogs bool
14 | MaxJobs int
15 | LogFormat string
16 | Builders []string
17 | Options map[string]string
18 | }
19 |
20 | func createTestCmd() (*cobra.Command, *nixOptions) {
21 | opts := nixOptions{}
22 |
23 | cmd := &cobra.Command{}
24 |
25 | nixopts.AddQuietNixOption(cmd, &opts.Quiet)
26 | nixopts.AddPrintBuildLogsNixOption(cmd, &opts.PrintBuildLogs)
27 | nixopts.AddMaxJobsNixOption(cmd, &opts.MaxJobs)
28 | nixopts.AddLogFormatNixOption(cmd, &opts.LogFormat)
29 | nixopts.AddBuildersNixOption(cmd, &opts.Builders)
30 | nixopts.AddOptionNixOption(cmd, &opts.Options)
31 |
32 | return cmd, &opts
33 | }
34 |
35 | func TestNixOptionsToArgsList(t *testing.T) {
36 | tests := []struct {
37 | name string
38 | // The command-line arguments passed to Cobra
39 | passedArgs []string
40 | // The expected arguments to be passed to Nix
41 | expected []string
42 | }{
43 | {
44 | name: "All fields zero-valued",
45 | passedArgs: []string{},
46 | expected: []string{},
47 | },
48 | {
49 | name: "Single boolean field",
50 | passedArgs: []string{"--quiet"},
51 | expected: []string{"--quiet"},
52 | },
53 | {
54 | name: "Integer field set",
55 | passedArgs: []string{"--max-jobs", "4"},
56 | expected: []string{"--max-jobs", "4"},
57 | },
58 | {
59 | name: "Integer field set to zero value",
60 | passedArgs: []string{"--max-jobs", "0"},
61 | expected: []string{"--max-jobs", "0"},
62 | },
63 | {
64 | name: "String field set",
65 | passedArgs: []string{"--log-format", "json"},
66 | expected: []string{"--log-format", "json"},
67 | },
68 | {
69 | name: "Slice field set",
70 | passedArgs: []string{"--builders", "builder1", "--builders", "builder2"},
71 | expected: []string{"--builders", "builder1", "--builders", "builder2"},
72 | },
73 | {
74 | name: "Map field set",
75 | passedArgs: []string{"--option", "option1=value1", "--option", "option2=value2"},
76 | expected: []string{"--option", "option1", "value1", "--option", "option2", "value2"},
77 | },
78 | {
79 | name: "Mixed fields set",
80 | passedArgs: []string{"--quiet", "--max-jobs", "2", "--log-format", "xml", "--builders", "builder1", "--option", "option1=value1", "--option", "option2=value2"},
81 | expected: []string{"--quiet", "--max-jobs", "2", "--log-format", "xml", "--builders", "builder1", "--option", "option1", "value1", "--option", "option2", "value2"},
82 | },
83 | }
84 |
85 | for _, tt := range tests {
86 | t.Run(tt.name, func(t *testing.T) {
87 | cmd, opts := createTestCmd()
88 |
89 | // Dummy execution of "command" for Cobra to parse flags
90 | cmd.SetArgs(tt.passedArgs)
91 | _ = cmd.Execute()
92 |
93 | args := nixopts.NixOptionsToArgsList(cmd.Flags(), opts)
94 |
95 | if !reflect.DeepEqual(args, tt.expected) {
96 | t.Errorf("NixOptionsToArgsList() = %v, want %v", args, tt.expected)
97 | }
98 | })
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/internal/cmd/nixopts/nixopts.go:
--------------------------------------------------------------------------------
1 | package nixopts
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | func addNixOptionBool(cmd *cobra.Command, dest *bool, name string, shorthand string, desc string) {
8 | if shorthand != "" {
9 | cmd.Flags().BoolVarP(dest, name, shorthand, false, desc)
10 | } else {
11 | cmd.Flags().BoolVar(dest, name, false, desc)
12 | }
13 | cmd.Flags().Lookup(name).Hidden = true
14 | }
15 |
16 | func addNixOptionInt(cmd *cobra.Command, dest *int, name string, shorthand string, desc string) {
17 | if shorthand != "" {
18 | cmd.Flags().IntVarP(dest, name, shorthand, 0, desc)
19 | } else {
20 | cmd.Flags().IntVar(dest, name, 0, desc)
21 | }
22 | cmd.Flags().Lookup(name).Hidden = true
23 | }
24 |
25 | func addNixOptionString(cmd *cobra.Command, dest *string, name string, shorthand string, desc string) {
26 | if shorthand != "" {
27 | cmd.Flags().StringVarP(dest, name, shorthand, "", desc)
28 | } else {
29 | cmd.Flags().StringVar(dest, name, "", desc)
30 | }
31 | cmd.Flags().Lookup(name).Hidden = true
32 | }
33 |
34 | func addNixOptionStringArray(cmd *cobra.Command, dest *[]string, name string, shorthand string, desc string) {
35 | if shorthand != "" {
36 | cmd.Flags().StringSliceVarP(dest, name, shorthand, nil, desc)
37 | } else {
38 | cmd.Flags().StringSliceVar(dest, name, nil, desc)
39 | }
40 | cmd.Flags().Lookup(name).Hidden = true
41 | }
42 |
43 | func addNixOptionStringMap(cmd *cobra.Command, dest *map[string]string, name string, shorthand string, desc string) {
44 | if shorthand != "" {
45 | cmd.Flags().StringToStringVarP(dest, name, shorthand, nil, desc)
46 | } else {
47 | cmd.Flags().StringToStringVar(dest, name, nil, desc)
48 | }
49 | cmd.Flags().Lookup(name).Hidden = true
50 | }
51 |
52 | func AddQuietNixOption(cmd *cobra.Command, dest *bool) {
53 | addNixOptionBool(cmd, dest, "quiet", "", "Decrease logging verbosity level")
54 | }
55 |
56 | func AddPrintBuildLogsNixOption(cmd *cobra.Command, dest *bool) {
57 | addNixOptionBool(cmd, dest, "print-build-logs", "L", "Decrease logging verbosity level")
58 | }
59 |
60 | func AddNoBuildOutputNixOption(cmd *cobra.Command, dest *bool) {
61 | addNixOptionBool(cmd, dest, "no-build-output", "Q", "Silence build output on stdout and stderr")
62 | }
63 |
64 | func AddShowTraceNixOption(cmd *cobra.Command, dest *bool) {
65 | addNixOptionBool(cmd, dest, "show-trace", "", "Print stack trace of evaluation errors")
66 | }
67 |
68 | func AddKeepGoingNixOption(cmd *cobra.Command, dest *bool) {
69 | addNixOptionBool(cmd, dest, "keep-going", "k", "Keep going until all builds are finished despite failures")
70 | }
71 |
72 | func AddKeepFailedNixOption(cmd *cobra.Command, dest *bool) {
73 | addNixOptionBool(cmd, dest, "keep-failed", "K", "Keep failed builds (usually in /tmp)")
74 | }
75 |
76 | func AddFallbackNixOption(cmd *cobra.Command, dest *bool) {
77 | addNixOptionBool(cmd, dest, "fallback", "", "If binary download fails, fall back on building from source")
78 | }
79 |
80 | func AddRefreshNixOption(cmd *cobra.Command, dest *bool) {
81 | addNixOptionBool(cmd, dest, "refresh", "", "Consider all previously downloaded files out-of-date")
82 | }
83 |
84 | func AddRepairNixOption(cmd *cobra.Command, dest *bool) {
85 | addNixOptionBool(cmd, dest, "repair", "", "Fix corrupted or missing store paths")
86 | }
87 |
88 | func AddImpureNixOption(cmd *cobra.Command, dest *bool) {
89 | addNixOptionBool(cmd, dest, "impure", "", "Allow access to mutable paths and repositories")
90 | }
91 |
92 | func AddOfflineNixOption(cmd *cobra.Command, dest *bool) {
93 | addNixOptionBool(cmd, dest, "offline", "", "Disable substituters and consider all previously downloaded files up-to-date.")
94 | }
95 |
96 | func AddNoNetNixOption(cmd *cobra.Command, dest *bool) {
97 | addNixOptionBool(cmd, dest, "no-net", "", "Disable substituters and set all network timeout settings to minimum")
98 | }
99 |
100 | func AddIncludesNixOption(cmd *cobra.Command, dest *[]string) {
101 | addNixOptionStringArray(cmd, dest, "--include", "I", "Add path to list of locations to look up <...> file names")
102 | }
103 |
104 | func AddMaxJobsNixOption(cmd *cobra.Command, dest *int) {
105 | addNixOptionInt(cmd, dest, "max-jobs", "j", "Max number of build jobs in parallel")
106 | }
107 |
108 | func AddCoresNixOption(cmd *cobra.Command, dest *int) {
109 | addNixOptionInt(cmd, dest, "cores", "", "Max number of CPU cores used (sets NIX_BUILD_CORES env variable)")
110 | }
111 |
112 | func AddBuildersNixOption(cmd *cobra.Command, dest *[]string) {
113 | addNixOptionStringArray(cmd, dest, "builders", "", "List of Nix remote builder addresses")
114 | }
115 |
116 | func AddLogFormatNixOption(cmd *cobra.Command, dest *string) {
117 | addNixOptionString(cmd, dest, "log-format", "", "Configure how output is formatted")
118 | }
119 |
120 | func AddOptionNixOption(cmd *cobra.Command, dest *map[string]string) {
121 | addNixOptionStringMap(cmd, dest, "option", "", "Set Nix config option (passed as 1 arg, requires = separator)")
122 | }
123 |
124 | func AddRecreateLockFileNixOption(cmd *cobra.Command, dest *bool) {
125 | addNixOptionBool(cmd, dest, "recreate-lock-file", "", "Recreate the flake's lock file from scratch")
126 | }
127 |
128 | func AddNoUpdateLockFileNixOption(cmd *cobra.Command, dest *bool) {
129 | addNixOptionBool(cmd, dest, "no-update-lock-file", "", "Do not allow any updates to the flake's lock file")
130 | }
131 |
132 | func AddNoWriteLockFileNixOption(cmd *cobra.Command, dest *bool) {
133 | addNixOptionBool(cmd, dest, "no-write-lock-file", "", "Do not write the flake's newly generated lock file")
134 | }
135 |
136 | func AddNoUseRegistriesNixOption(cmd *cobra.Command, dest *bool) {
137 | addNixOptionBool(cmd, dest, "no-use-registries", "", "Don't allow lookups in the flake registries")
138 | addNixOptionBool(cmd, dest, "no-registries", "", "Don't allow lookups in the flake registries")
139 | // TODO: add deprecation notice for --no-registries?
140 | }
141 |
142 | func AddCommitLockFileNixOption(cmd *cobra.Command, dest *bool) {
143 | addNixOptionBool(cmd, dest, "commit-lock-file", "", "Commit changes to the flake's lock file")
144 | }
145 |
146 | func AddUpdateInputNixOption(cmd *cobra.Command, dest *[]string) {
147 | addNixOptionStringArray(cmd, dest, "update-input", "", "Update a specific flake input")
148 | }
149 |
150 | func AddOverrideInputNixOption(cmd *cobra.Command, dest *map[string]string) {
151 | addNixOptionStringMap(cmd, dest, "override-input", "", "Override a specific flake input (passed as 1 arg, requires = separator)")
152 | }
153 |
--------------------------------------------------------------------------------
/internal/cmd/opts/opts.go:
--------------------------------------------------------------------------------
1 | package cmdOpts
2 |
3 | import "github.com/nix-community/nixos-cli/internal/configuration"
4 |
5 | type MainOpts struct {
6 | ColorAlways bool
7 | ConfigValues map[string]string
8 | }
9 |
10 | type AliasesOpts struct {
11 | DisplayJson bool
12 | }
13 |
14 | type ApplyOpts struct {
15 | Dry bool
16 | InstallBootloader bool
17 | NoActivate bool
18 | NoBoot bool
19 | OutputPath string
20 | ProfileName string
21 | Specialisation string
22 | GenerationTag string
23 | UpgradeChannels bool
24 | UpgradeAllChannels bool
25 | UseNom bool
26 | Verbose bool
27 | BuildVM bool
28 | BuildVMWithBootloader bool
29 | AlwaysConfirm bool
30 | FlakeRef string
31 |
32 | NixOptions ApplyNixOptions
33 | }
34 |
35 | type ApplyNixOptions struct {
36 | Quiet bool
37 | PrintBuildLogs bool
38 | NoBuildOutput bool
39 | ShowTrace bool
40 | KeepGoing bool
41 | KeepFailed bool
42 | Fallback bool
43 | Refresh bool
44 | Repair bool
45 | Impure bool
46 | Offline bool
47 | NoNet bool
48 | MaxJobs int
49 | Cores int
50 | Builders []string
51 | LogFormat string
52 | Includes []string
53 | Options map[string]string
54 |
55 | RecreateLockFile bool
56 | NoUpdateLockFile bool
57 | NoWriteLockFile bool
58 | NoUseRegistries bool
59 | CommitLockFile bool
60 | UpdateInputs []string
61 | OverrideInputs map[string]string
62 | }
63 |
64 | type EnterOpts struct {
65 | Command string
66 | CommandArray []string
67 | RootLocation string
68 | System string
69 | Silent bool
70 | Verbose bool
71 | }
72 |
73 | type FeaturesOpts struct {
74 | DisplayJson bool
75 | }
76 |
77 | type GenerationOpts struct {
78 | ProfileName string
79 | }
80 |
81 | type GenerationDiffOpts struct {
82 | Before uint
83 | After uint
84 | Verbose bool
85 | }
86 |
87 | type GenerationDeleteOpts struct {
88 | All bool
89 | LowerBound uint64
90 | // This ideally should be a uint64 to match types,
91 | // but Cobra's pflags does not support this type yet.
92 | Keep []uint
93 | MinimumToKeep uint64
94 | OlderThan string
95 | UpperBound uint64
96 | AlwaysConfirm bool
97 | // This ideally should be a uint64 to match types,
98 | // but Cobra's pflags does not support this type yet.
99 | Remove []uint
100 | Verbose bool
101 | }
102 |
103 | type GenerationListOpts struct {
104 | DisplayJson bool
105 | DisplayTable bool
106 | }
107 |
108 | type GenerationSwitchOpts struct {
109 | Dry bool
110 | Specialisation string
111 | Verbose bool
112 | AlwaysConfirm bool
113 | Generation uint
114 | }
115 |
116 | type GenerationRollbackOpts struct {
117 | Dry bool
118 | Specialisation string
119 | Verbose bool
120 | AlwaysConfirm bool
121 | }
122 |
123 | type InfoOpts struct {
124 | DisplayJson bool
125 | DisplayMarkdown bool
126 | }
127 |
128 | type InitOpts struct {
129 | Directory string
130 | ForceWrite bool
131 | NoFSGeneration bool
132 | Root string
133 | ShowHardwareConfig bool
134 | }
135 |
136 | type InstallOpts struct {
137 | Channel string
138 | NoBootloader bool
139 | NoChannelCopy bool
140 | NoRootPassword bool
141 | Root string
142 | SystemClosure string
143 | Verbose bool
144 | FlakeRef *configuration.FlakeRef
145 |
146 | NixOptions struct {
147 | Quiet bool
148 | PrintBuildLogs bool
149 | NoBuildOutput bool
150 | ShowTrace bool
151 | KeepGoing bool
152 | KeepFailed bool
153 | Fallback bool
154 | Refresh bool
155 | Repair bool
156 | Impure bool
157 | Offline bool
158 | NoNet bool
159 | MaxJobs int
160 | Cores int
161 | LogFormat string
162 | Includes []string
163 | Options map[string]string
164 |
165 | RecreateLockFile bool
166 | NoUpdateLockFile bool
167 | NoWriteLockFile bool
168 | NoUseRegistries bool
169 | CommitLockFile bool
170 | UpdateInputs []string
171 | OverrideInputs map[string]string
172 | }
173 | }
174 |
175 | type OptionOpts struct {
176 | Interactive bool
177 | NixPathIncludes []string
178 | DisplayJson bool
179 | NoUseCache bool
180 | DisplayValueOnly bool
181 | MinScore int64
182 | OptionInput string
183 | FlakeRef string
184 | }
185 |
186 | type ReplOpts struct {
187 | NixPathIncludes []string
188 | FlakeRef string
189 | }
190 |
--------------------------------------------------------------------------------
/internal/cmd/utils/confirmation.go:
--------------------------------------------------------------------------------
1 | package cmdUtils
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 |
8 | "github.com/fatih/color"
9 | )
10 |
11 | func ConfirmationInput(msg string) (bool, error) {
12 | var input string
13 |
14 | fmt.Fprintf(os.Stderr, "%s\n[y/n]: ", color.GreenString("|> %s", msg))
15 |
16 | _, err := fmt.Scanln(&input)
17 | if err != nil {
18 | return false, err
19 | }
20 |
21 | if len(input) == 0 {
22 | return false, err
23 | }
24 |
25 | input = strings.ToLower(strings.TrimSpace(input))
26 |
27 | return input[0] == 'y', nil
28 | }
29 |
--------------------------------------------------------------------------------
/internal/cmd/utils/utils.go:
--------------------------------------------------------------------------------
1 | package cmdUtils
2 |
3 | import (
4 | "errors"
5 | "os"
6 |
7 | tea "github.com/charmbracelet/bubbletea"
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | func SetHelpFlagText(cmd *cobra.Command) {
12 | cmd.Flags().BoolP("help", "h", false, "Show this help menu")
13 | }
14 |
15 | var CommandError = errors.New("command error")
16 |
17 | // Replace a returned error with the generic CommandError, and.
18 | // exit with a non-zero exit code. This is to avoid extra error
19 | // messages being printed when a command function defined with
20 | // RunE returns a non-nil error.
21 | func CommandErrorHandler(err error) error {
22 | if err != nil {
23 | os.Exit(1)
24 |
25 | return CommandError
26 | }
27 | return nil
28 | }
29 |
30 | func ConfigureBubbleTeaLogger(prefix string) (func(), error) {
31 | if os.Getenv("NIXOS_CLI_DEBUG_MODE") == "" {
32 | return func() {}, nil
33 | }
34 |
35 | file, err := tea.LogToFile("debug.log", prefix)
36 |
37 | return func() {
38 | if err != nil || file == nil {
39 | return
40 | }
41 | _ = file.Close()
42 | }, err
43 | }
44 |
--------------------------------------------------------------------------------
/internal/configuration/configuration.go:
--------------------------------------------------------------------------------
1 | package configuration
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/nix-community/nixos-cli/internal/build"
7 | "github.com/nix-community/nixos-cli/internal/logger"
8 | "github.com/nix-community/nixos-cli/internal/settings"
9 | "github.com/nix-community/nixos-cli/internal/system"
10 | "github.com/spf13/pflag"
11 | )
12 |
13 | type SystemBuildOptions struct {
14 | ResultLocation string
15 | DryBuild bool
16 | UseNom bool
17 | GenerationTag string
18 | Verbose bool
19 |
20 | // Command-line flags that were passed for the command context.
21 | // This is needed to determine the proper Nix options to pass
22 | // when building, if any were passed through.
23 | CmdFlags *pflag.FlagSet
24 | NixOpts any
25 | Env map[string]string
26 | ExtraArgs []string
27 | }
28 |
29 | type Configuration interface {
30 | SetBuilder(builder system.CommandRunner)
31 | EvalAttribute(attr string) (*string, error)
32 | BuildSystem(buildType SystemBuildType, opts *SystemBuildOptions) (string, error)
33 | }
34 |
35 | type AttributeEvaluationError struct {
36 | Attribute string
37 | EvaluationOutput string
38 | }
39 |
40 | func (e *AttributeEvaluationError) Error() string {
41 | return fmt.Sprintf("failed to evaluate attribute %s", e.Attribute)
42 | }
43 |
44 | func FindConfiguration(log *logger.Logger, cfg *settings.Settings, includes []string, verbose bool) (Configuration, error) {
45 | if buildOpts.Flake == "true" {
46 | if verbose {
47 | log.Info("looking for flake configuration")
48 | }
49 |
50 | f, err := FlakeRefFromEnv(cfg.ConfigLocation)
51 | if err != nil {
52 | return nil, err
53 | }
54 |
55 | if err := f.InferSystemFromHostnameIfNeeded(); err != nil {
56 | return nil, err
57 | }
58 |
59 | if verbose {
60 | log.Infof("found flake configuration: %s#%s", f.URI, f.System)
61 | }
62 |
63 | return f, nil
64 | } else {
65 | c, err := FindLegacyConfiguration(log, includes, verbose)
66 | if err != nil {
67 | return nil, err
68 | }
69 |
70 | if verbose {
71 | log.Infof("found legacy configuration at %s", c)
72 | }
73 |
74 | return c, nil
75 | }
76 | }
77 |
78 | type SystemBuildType int
79 |
80 | const (
81 | SystemBuildTypeSystem SystemBuildType = iota
82 | SystemBuildTypeSystemActivation
83 | SystemBuildTypeVM
84 | SystemBuildTypeVMWithBootloader
85 | )
86 |
87 | func (b SystemBuildType) BuildAttr() string {
88 | switch b {
89 | case SystemBuildTypeSystem, SystemBuildTypeSystemActivation:
90 | if buildOpts.Flake == "true" {
91 | return "toplevel"
92 | } else {
93 | return "system"
94 | }
95 | case SystemBuildTypeVM:
96 | return "vm"
97 | case SystemBuildTypeVMWithBootloader:
98 | return "vmWithBootLoader"
99 | default:
100 | panic("unknown build type")
101 | }
102 | }
103 |
104 | func (b SystemBuildType) IsVM() bool {
105 | return b == SystemBuildTypeVM || b == SystemBuildTypeVMWithBootloader
106 | }
107 |
108 | func (b SystemBuildType) IsSystem() bool {
109 | return b == SystemBuildTypeSystem || b == SystemBuildTypeSystemActivation
110 | }
111 |
--------------------------------------------------------------------------------
/internal/configuration/configuration_test.go:
--------------------------------------------------------------------------------
1 | package configuration_test
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/nix-community/nixos-cli/internal/configuration"
8 | )
9 |
10 | func TestFlakeRefFromString(t *testing.T) {
11 | tests := []struct {
12 | input string
13 | expected *configuration.FlakeRef
14 | }{
15 | {
16 | input: "github:owner/repo#linux",
17 | expected: &configuration.FlakeRef{
18 | URI: "github:owner/repo",
19 | System: "linux",
20 | },
21 | },
22 | {
23 | input: "github:owner/repo",
24 | expected: &configuration.FlakeRef{
25 | URI: "github:owner/repo",
26 | System: "",
27 | },
28 | },
29 | {
30 | input: "github:owner/repo#",
31 | expected: &configuration.FlakeRef{
32 | URI: "github:owner/repo",
33 | System: "",
34 | },
35 | },
36 | {
37 | input: "#linux",
38 | expected: &configuration.FlakeRef{
39 | URI: "",
40 | System: "linux",
41 | },
42 | },
43 | {
44 | input: "",
45 | expected: &configuration.FlakeRef{
46 | URI: "",
47 | System: "",
48 | },
49 | },
50 | {
51 | input: "github:owner/repo#linux#extra",
52 | expected: &configuration.FlakeRef{
53 | URI: "github:owner/repo",
54 | System: "linux#extra",
55 | },
56 | },
57 | }
58 |
59 | for _, tt := range tests {
60 | t.Run(tt.input, func(t *testing.T) {
61 | result := configuration.FlakeRefFromString(tt.input)
62 |
63 | if !reflect.DeepEqual(result, tt.expected) {
64 | t.Errorf("FlakeRefFromString(%q) = %+v, want %+v", tt.input, result, tt.expected)
65 | }
66 | })
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/internal/configuration/flake.go:
--------------------------------------------------------------------------------
1 | package configuration
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "os/exec"
8 | "strings"
9 |
10 | "github.com/nix-community/nixos-cli/internal/cmd/nixopts"
11 | "github.com/nix-community/nixos-cli/internal/system"
12 | )
13 |
14 | type FlakeRef struct {
15 | URI string
16 | System string
17 |
18 | // Builder is used to build the flake ref. They must have Nix installed.
19 | Builder system.CommandRunner
20 | }
21 |
22 | func FlakeRefFromString(s string) *FlakeRef {
23 | split := strings.Index(s, "#")
24 |
25 | if split > -1 {
26 | return &FlakeRef{
27 | URI: s[:split],
28 | System: s[split+1:],
29 | }
30 | }
31 |
32 | return &FlakeRef{
33 | URI: s,
34 | System: "",
35 | }
36 | }
37 |
38 | func FlakeRefFromEnv(defaultLocation string) (*FlakeRef, error) {
39 | nixosConfig, set := os.LookupEnv("NIXOS_CONFIG")
40 | if !set {
41 | nixosConfig = defaultLocation
42 | }
43 |
44 | if nixosConfig == "" {
45 | return nil, fmt.Errorf("NIXOS_CONFIG is not set")
46 | }
47 |
48 | return FlakeRefFromString(nixosConfig), nil
49 | }
50 |
51 | func (f *FlakeRef) InferSystemFromHostnameIfNeeded() error {
52 | if f.System == "" {
53 | hostname, err := os.Hostname()
54 | if err != nil {
55 | return err
56 | }
57 |
58 | f.System = hostname
59 | }
60 |
61 | return nil
62 | }
63 |
64 | func (f *FlakeRef) SetBuilder(builder system.CommandRunner) {
65 | f.Builder = builder
66 | }
67 |
68 | func (f *FlakeRef) EvalAttribute(attr string) (*string, error) {
69 | evalArg := fmt.Sprintf(`%s#nixosConfigurations.%s.config.%s`, f.URI, f.System, attr)
70 | argv := []string{"nix", "eval", evalArg}
71 |
72 | var stdout bytes.Buffer
73 | var stderr bytes.Buffer
74 |
75 | cmd := exec.Command(argv[0], argv[1:]...)
76 | cmd.Stdout = &stdout
77 | cmd.Stderr = &stderr
78 |
79 | err := cmd.Run()
80 | if err != nil {
81 | return nil, &AttributeEvaluationError{
82 | Attribute: attr,
83 | EvaluationOutput: strings.TrimSpace(stderr.String()),
84 | }
85 | }
86 |
87 | value := strings.TrimSpace(stdout.String())
88 |
89 | return &value, nil
90 | }
91 |
92 | func (f *FlakeRef) BuildSystem(buildType SystemBuildType, opts *SystemBuildOptions) (string, error) {
93 | nixCommand := "nix"
94 | if opts.UseNom {
95 | nixCommand = "nom"
96 | }
97 |
98 | systemAttribute := fmt.Sprintf("%s#nixosConfigurations.%s.config.system.build.%s", f.URI, f.System, buildType.BuildAttr())
99 |
100 | argv := []string{nixCommand, "build", systemAttribute, "--print-out-paths"}
101 |
102 | if opts.ResultLocation != "" {
103 | argv = append(argv, "--out-link", opts.ResultLocation)
104 | } else {
105 | argv = append(argv, "--no-link")
106 | }
107 |
108 | if opts.DryBuild {
109 | argv = append(argv, "--dry-run")
110 | }
111 |
112 | if opts.NixOpts != nil {
113 | argv = append(argv, nixopts.NixOptionsToArgsList(opts.CmdFlags, opts.NixOpts)...)
114 | }
115 |
116 | if opts.ExtraArgs != nil {
117 | argv = append(argv, opts.ExtraArgs...)
118 | }
119 |
120 | if opts.Verbose {
121 | argv = append(argv, "-v")
122 | f.Builder.Logger().CmdArray(argv)
123 | }
124 |
125 | var stdout bytes.Buffer
126 | cmd := system.NewCommand(nixCommand, argv[1:]...)
127 | cmd.Stdout = &stdout
128 |
129 | if opts.GenerationTag != "" {
130 | cmd.SetEnv("NIXOS_GENERATION_TAG", opts.GenerationTag)
131 | }
132 |
133 | for k, v := range opts.Env {
134 | cmd.SetEnv(k, v)
135 | }
136 |
137 | if f.Builder == nil {
138 | panic("FlakeRef.Builder is nil")
139 | }
140 |
141 | _, err := f.Builder.Run(cmd)
142 |
143 | return strings.Trim(stdout.String(), "\n "), err
144 | }
145 |
--------------------------------------------------------------------------------
/internal/configuration/legacy.go:
--------------------------------------------------------------------------------
1 | package configuration
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 | "strings"
10 |
11 | "github.com/nix-community/nixos-cli/internal/cmd/nixopts"
12 | "github.com/nix-community/nixos-cli/internal/logger"
13 | "github.com/nix-community/nixos-cli/internal/system"
14 | )
15 |
16 | type LegacyConfiguration struct {
17 | Includes []string
18 | ConfigDirname string
19 |
20 | // Builder is used to build the legacy system. They must have Nix installed.
21 | Builder system.CommandRunner
22 | }
23 |
24 | func FindLegacyConfiguration(log *logger.Logger, includes []string, verbose bool) (*LegacyConfiguration, error) {
25 | if verbose {
26 | log.Infof("looking for legacy configuration")
27 | }
28 |
29 | var configuration string
30 | if nixosCfg, set := os.LookupEnv("NIXOS_CONFIG"); set {
31 | if verbose {
32 | log.Info("$NIXOS_CONFIG is set, using automatically")
33 | }
34 | configuration = nixosCfg
35 | }
36 |
37 | if configuration == "" && includes != nil {
38 | for _, include := range includes {
39 | if strings.HasPrefix(include, "nixos-config=") {
40 | configuration = strings.TrimPrefix(include, "nixos-config=")
41 | break
42 | }
43 | }
44 | }
45 |
46 | if configuration == "" {
47 | if verbose {
48 | log.Infof("$NIXOS_CONFIG not set, using $NIX_PATH to find configuration")
49 | }
50 |
51 | nixPath := strings.Split(os.Getenv("NIX_PATH"), ":")
52 | for _, entry := range nixPath {
53 | if strings.HasPrefix(entry, "nixos-config=") {
54 | configuration = strings.TrimPrefix(entry, "nixos-config=")
55 | break
56 | }
57 | }
58 |
59 | if configuration == "" {
60 | return nil, fmt.Errorf("expected 'nixos-config' attribute to exist in NIX_PATH")
61 | }
62 | }
63 |
64 | configFileStat, err := os.Stat(configuration)
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | if configFileStat.IsDir() {
70 | defaultNix := filepath.Join(configuration, "default.nix")
71 |
72 | info, err := os.Stat(defaultNix)
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | if info.IsDir() {
78 | return nil, fmt.Errorf("%v is a directory, not a file", defaultNix)
79 | }
80 | }
81 |
82 | return &LegacyConfiguration{
83 | Includes: includes,
84 | ConfigDirname: configuration,
85 | }, nil
86 | }
87 |
88 | func (l *LegacyConfiguration) SetBuilder(builder system.CommandRunner) {
89 | l.Builder = builder
90 | }
91 |
92 | func (l *LegacyConfiguration) EvalAttribute(attr string) (*string, error) {
93 | configAttr := fmt.Sprintf("config.%s", attr)
94 | argv := []string{"nix-instantiate", "--eval", "", "-A", configAttr}
95 |
96 | for _, v := range l.Includes {
97 | argv = append(argv, "-I", v)
98 | }
99 |
100 | var stdout bytes.Buffer
101 | var stderr bytes.Buffer
102 |
103 | cmd := exec.Command(argv[0], argv[1:]...)
104 | cmd.Stdout = &stdout
105 | cmd.Stderr = &stderr
106 |
107 | err := cmd.Run()
108 | if err != nil {
109 | return nil, &AttributeEvaluationError{
110 | Attribute: attr,
111 | EvaluationOutput: strings.TrimSpace(stderr.String()),
112 | }
113 | }
114 |
115 | value := strings.TrimSpace(stdout.String())
116 |
117 | return &value, nil
118 | }
119 |
120 | func (l *LegacyConfiguration) BuildSystem(buildType SystemBuildType, opts *SystemBuildOptions) (string, error) {
121 | nixCommand := "nix-build"
122 | if opts.UseNom {
123 | nixCommand = "nom-build"
124 | }
125 |
126 | argv := []string{nixCommand, "", "-A", buildType.BuildAttr()}
127 |
128 | // Mimic `nixos-rebuild` behavior of using -k option
129 | // for all commands except for switch and boot
130 | if buildType != SystemBuildTypeSystemActivation {
131 | argv = append(argv, "-k")
132 | }
133 |
134 | if opts.NixOpts != nil {
135 | argv = append(argv, nixopts.NixOptionsToArgsList(opts.CmdFlags, opts.NixOpts)...)
136 | }
137 |
138 | if opts.ResultLocation != "" {
139 | argv = append(argv, "--out-link", opts.ResultLocation)
140 | } else {
141 | argv = append(argv, "--no-out-link")
142 | }
143 |
144 | if opts.ExtraArgs != nil {
145 | argv = append(argv, opts.ExtraArgs...)
146 | }
147 |
148 | if opts.Verbose {
149 | argv = append(argv, "-v")
150 | l.Builder.Logger().CmdArray(argv)
151 | }
152 |
153 | var stdout bytes.Buffer
154 | cmd := system.NewCommand(nixCommand, argv[1:]...)
155 | cmd.Stdout = &stdout
156 |
157 | if opts.GenerationTag != "" {
158 | cmd.SetEnv("NIXOS_GENERATION_TAG", opts.GenerationTag)
159 | }
160 |
161 | if l.Builder == nil {
162 | panic("LegacyConfiguration.Builder is nil")
163 | }
164 |
165 | for k, v := range opts.Env {
166 | cmd.SetEnv(k, v)
167 | }
168 |
169 | _, err := l.Builder.Run(cmd)
170 |
171 | return strings.Trim(stdout.String(), "\n "), err
172 | }
173 |
--------------------------------------------------------------------------------
/internal/constants/constants.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | const (
4 | NixProfileDirectory = "/nix/var/nix/profiles"
5 | NixSystemProfileDirectory = NixProfileDirectory + "/system-profiles"
6 | DefaultConfigLocation = "/etc/nixos-cli/config.toml"
7 | CurrentSystem = "/run/current-system"
8 | NixOSMarker = "/etc/NIXOS"
9 | NixChannelDirectory = NixProfileDirectory + "/per-user/root/channels"
10 | )
11 |
--------------------------------------------------------------------------------
/internal/generation/completion.go:
--------------------------------------------------------------------------------
1 | package generation
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "regexp"
7 | "slices"
8 | "sort"
9 | "strconv"
10 |
11 | "github.com/nix-community/nixos-cli/internal/constants"
12 | "github.com/nix-community/nixos-cli/internal/logger"
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | var genLinkRegex = regexp.MustCompile(`-(\d+)-link$`)
17 |
18 | func CompleteProfileFlag(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
19 | profiles := []string{"system"}
20 |
21 | entries, err := os.ReadDir(constants.NixSystemProfileDirectory)
22 | if err != nil {
23 | return []string{}, cobra.ShellCompDirectiveNoFileComp
24 | }
25 |
26 | for _, v := range entries {
27 | name := v.Name()
28 |
29 | if matches := genLinkRegex.FindStringSubmatch(name); len(matches) > 0 {
30 | continue
31 | }
32 |
33 | profiles = append(profiles, name)
34 | }
35 |
36 | sort.Strings(profiles)
37 |
38 | return profiles, cobra.ShellCompDirectiveNoFileComp
39 | }
40 |
41 | func CompleteGenerationNumber(profile *string, limit int) cobra.CompletionFunc {
42 | return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
43 | log := logger.FromContext(cmd.Context())
44 |
45 | if limit != 0 && len(args) >= limit {
46 | return []string{}, cobra.ShellCompDirectiveNoFileComp
47 | }
48 |
49 | generations, err := CollectGenerationsInProfile(log, *profile)
50 | if err != nil {
51 | return []string{}, cobra.ShellCompDirectiveNoFileComp
52 | }
53 |
54 | exclude := []uint64{}
55 | for _, v := range args {
56 | parsed, err := strconv.ParseUint(v, 10, 64)
57 | if err != nil {
58 | continue
59 | }
60 | exclude = append(exclude, parsed)
61 | }
62 |
63 | genNumbers := []string{}
64 | for _, v := range generations {
65 | if slices.Contains(exclude, v.Number) {
66 | continue
67 | }
68 | genNumber := fmt.Sprint(v.Number)
69 | if v.Description != "" {
70 | genNumber += "\t" + v.Description
71 | }
72 | genNumbers = append(genNumbers, genNumber)
73 | }
74 |
75 | sort.Strings(genNumbers)
76 |
77 | return genNumbers, cobra.ShellCompDirectiveNoFileComp
78 | }
79 | }
80 |
81 | func CompleteGenerationNumberFlag(profile *string) cobra.CompletionFunc {
82 | return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
83 | log := logger.FromContext(cmd.Context())
84 |
85 | generations, err := CollectGenerationsInProfile(log, *profile)
86 | if err != nil {
87 | return []string{}, cobra.ShellCompDirectiveNoFileComp
88 | }
89 |
90 | genNumbers := []string{}
91 | for _, v := range generations {
92 | genNumber := fmt.Sprint(v.Number)
93 | if v.Description != "" {
94 | genNumber += "\t" + v.Description
95 | }
96 | genNumbers = append(genNumbers, genNumber)
97 | }
98 |
99 | sort.Strings(genNumbers)
100 |
101 | return genNumbers, cobra.ShellCompDirectiveNoFileComp
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/internal/generation/diff.go:
--------------------------------------------------------------------------------
1 | package generation
2 |
3 | import (
4 | "os/exec"
5 |
6 | "github.com/nix-community/nixos-cli/internal/logger"
7 | "github.com/nix-community/nixos-cli/internal/system"
8 | )
9 |
10 | type DiffCommandOptions struct {
11 | UseNvd bool
12 | Verbose bool
13 | }
14 |
15 | func RunDiffCommand(log *logger.Logger, s system.CommandRunner, before string, after string, opts *DiffCommandOptions) error {
16 | useNvd := opts.UseNvd
17 |
18 | if opts.UseNvd {
19 | nvdPath, _ := exec.LookPath("nvd")
20 | nvdFound := nvdPath != ""
21 | if !nvdFound {
22 | log.Warn("use_nvd is specified in config, but `nvd` is not executable")
23 | log.Warn("falling back to `nix store diff-closures`")
24 | useNvd = false
25 | }
26 | }
27 |
28 | argv := []string{"nix", "store", "diff-closures", before, after}
29 | if useNvd {
30 | argv = []string{"nvd", "diff", before, after}
31 | }
32 |
33 | if opts.Verbose {
34 | s.Logger().CmdArray(argv)
35 | }
36 |
37 | cmd := system.NewCommand(argv[0], argv[1:]...)
38 |
39 | _, err := s.Run(cmd)
40 |
41 | return err
42 | }
43 |
--------------------------------------------------------------------------------
/internal/generation/specialisations.go:
--------------------------------------------------------------------------------
1 | package generation
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os/exec"
7 | "path/filepath"
8 | "sort"
9 | "strings"
10 |
11 | "github.com/nix-community/nixos-cli/internal/configuration"
12 | "github.com/nix-community/nixos-cli/internal/logger"
13 | "github.com/nix-community/nixos-cli/internal/settings"
14 | "github.com/spf13/cobra"
15 | )
16 |
17 | func CollectSpecialisations(generationDirname string) ([]string, error) {
18 | var specialisations []string
19 |
20 | specialisationsGlob := filepath.Join(generationDirname, "specialisation", "*")
21 |
22 | specialisationsMatches, err := filepath.Glob(specialisationsGlob)
23 | if err != nil {
24 | return nil, err
25 | } else {
26 | for _, match := range specialisationsMatches {
27 | specialisations = append(specialisations, filepath.Base(match))
28 | }
29 | }
30 |
31 | sort.Strings(specialisations)
32 |
33 | return specialisations, nil
34 | }
35 |
36 | func CollectSpecialisationsFromConfig(cfg configuration.Configuration) []string {
37 | var argv []string
38 |
39 | switch c := cfg.(type) {
40 | case *configuration.FlakeRef:
41 | attr := fmt.Sprintf("%s#nixosConfigurations.%s.config.specialisation", c.URI, c.System)
42 | argv = []string{"nix", "eval", attr, "--apply", "builtins.attrNames", "--json"}
43 | case *configuration.LegacyConfiguration:
44 | argv = []string{
45 | "nix-instantiate", "--eval", "--json", "--expr", "builtins.attrNames",
46 | "builtins.attrNames (import {}).config.specialisation",
47 | }
48 | }
49 |
50 | cmd := exec.Command(argv[0], argv[1:]...)
51 |
52 | stdout, err := cmd.Output()
53 | if err != nil {
54 | return []string{}
55 | }
56 |
57 | specialisations := []string{}
58 |
59 | err = json.Unmarshal(stdout, &specialisations)
60 | if err != nil {
61 | return []string{}
62 | }
63 |
64 | return specialisations
65 | }
66 |
67 | func CompleteSpecialisationFlag(generationDirname string) cobra.CompletionFunc {
68 | return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
69 | specialisations, err := CollectSpecialisations(generationDirname)
70 | if err != nil {
71 | return []string{}, cobra.ShellCompDirectiveNoFileComp
72 | }
73 |
74 | candidates := []string{}
75 |
76 | for _, specialisation := range specialisations {
77 | if specialisation == toComplete {
78 | return specialisations, cobra.ShellCompDirectiveNoFileComp
79 | }
80 |
81 | if strings.HasPrefix(specialisation, toComplete) {
82 | candidates = append(candidates, specialisation)
83 | }
84 | }
85 |
86 | return candidates, cobra.ShellCompDirectiveNoFileComp
87 | }
88 | }
89 |
90 | func CompleteSpecialisationFlagFromConfig(flakeRefStr string, includes []string) cobra.CompletionFunc {
91 | return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
92 | log := logger.FromContext(cmd.Context())
93 | cfg := settings.FromContext(cmd.Context())
94 |
95 | var nixConfig configuration.Configuration
96 | if flakeRefStr != "" {
97 | nixConfig = configuration.FlakeRefFromString(flakeRefStr)
98 | } else {
99 | c, err := configuration.FindConfiguration(log, cfg, includes, false)
100 | if err != nil {
101 | log.Errorf("failed to find configuration: %v", err)
102 | return []string{}, cobra.ShellCompDirectiveNoFileComp
103 | }
104 | nixConfig = c
105 | }
106 |
107 | if nixConfig == nil {
108 | log.Error("config is nil")
109 | return []string{}, cobra.ShellCompDirectiveNoFileComp
110 | }
111 |
112 | specialisations := CollectSpecialisationsFromConfig(nixConfig)
113 |
114 | candidates := []string{}
115 |
116 | for _, specialisation := range specialisations {
117 | if specialisation == toComplete {
118 | return []string{specialisation}, cobra.ShellCompDirectiveNoFileComp
119 | }
120 |
121 | if strings.HasPrefix(specialisation, toComplete) {
122 | candidates = append(candidates, specialisation)
123 | }
124 | }
125 |
126 | return candidates, cobra.ShellCompDirectiveNoFileComp
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/internal/logger/context.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import "context"
4 |
5 | type loggerCtxKeyType string
6 |
7 | const loggerCtxKey loggerCtxKeyType = "logger"
8 |
9 | func WithLogger(ctx context.Context, logger *Logger) context.Context {
10 | return context.WithValue(ctx, loggerCtxKey, logger)
11 | }
12 |
13 | func FromContext(ctx context.Context) *Logger {
14 | logger, ok := ctx.Value(loggerCtxKey).(*Logger)
15 | if !ok {
16 | panic("logger not present in context")
17 | }
18 | return logger
19 | }
20 |
--------------------------------------------------------------------------------
/internal/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "github.com/fatih/color"
8 | "github.com/nix-community/nixos-cli/internal/utils"
9 | )
10 |
11 | type Logger struct {
12 | print *log.Logger
13 | info *log.Logger
14 | warn *log.Logger
15 | error *log.Logger
16 |
17 | level LogLevel
18 | stepNumber uint
19 | stepsEnabled bool
20 | }
21 |
22 | type LogLevel int
23 |
24 | const (
25 | LogLevelInfo LogLevel = 0
26 | LogLevelWarn LogLevel = 1
27 | LogLevelError LogLevel = 2
28 | LogLevelSilent LogLevel = 3
29 | )
30 |
31 | func NewLogger() *Logger {
32 | green := color.New(color.FgGreen)
33 | boldYellow := color.New(color.FgYellow).Add(color.Bold)
34 | boldRed := color.New(color.FgRed).Add(color.Bold)
35 |
36 | return &Logger{
37 | print: log.New(os.Stderr, "", 0),
38 | info: log.New(os.Stderr, green.Sprint("info: "), 0),
39 | warn: log.New(os.Stderr, boldYellow.Sprint("warning: "), 0),
40 | error: log.New(os.Stderr, boldRed.Sprint("error: "), 0),
41 | stepNumber: 0,
42 | // Some commands call other subcommands through forks, such.
43 | // as `install` calling `enter`. For those, step numbers can
44 | // be confusing.
45 | stepsEnabled: os.Getenv("NIXOS_CLI_DISABLE_STEPS") == "",
46 | }
47 | }
48 |
49 | func (l *Logger) Print(v ...any) {
50 | l.print.Print(v...)
51 | }
52 |
53 | func (l *Logger) Printf(format string, v ...any) {
54 | l.print.Printf(format, v...)
55 | }
56 |
57 | func (l *Logger) Info(v ...any) {
58 | if l.level > LogLevelInfo {
59 | return
60 | }
61 | l.info.Println(v...)
62 | }
63 |
64 | func (l *Logger) Infof(format string, v ...any) {
65 | if l.level > LogLevelInfo {
66 | return
67 | }
68 | l.info.Printf(format+"\n", v...)
69 | }
70 |
71 | func (l *Logger) Warn(v ...any) {
72 | if l.level > LogLevelWarn {
73 | return
74 | }
75 | l.warn.Println(v...)
76 | }
77 |
78 | func (l *Logger) Warnf(format string, v ...any) {
79 | if l.level > LogLevelWarn {
80 | return
81 | }
82 | l.warn.Printf(format+"\n", v...)
83 | }
84 |
85 | func (l *Logger) Error(v ...any) {
86 | if l.level > LogLevelError {
87 | return
88 | }
89 | l.error.Println(v...)
90 | }
91 |
92 | func (l *Logger) Errorf(format string, v ...any) {
93 | if l.level > LogLevelError {
94 | return
95 | }
96 | l.error.Printf(format+"\n", v...)
97 | }
98 |
99 | func (l *Logger) CmdArray(argv []string) {
100 | if l.level > LogLevelInfo {
101 | return
102 | }
103 |
104 | msg := color.New(color.FgBlue).Sprintf("$ %v", utils.EscapeAndJoinArgs(argv))
105 | l.print.Printf("%v\n", msg)
106 | }
107 |
108 | func (l *Logger) Step(message string) {
109 | // Replace step numbers with generic l.Info() calls if
110 | // steps are disabled, to increase clarity in steps.
111 | if !l.stepsEnabled {
112 | l.Info(message)
113 | return
114 | }
115 |
116 | if l.level > LogLevelInfo {
117 | return
118 | }
119 |
120 | l.stepNumber++
121 | if l.stepNumber > 1 {
122 | l.print.Println()
123 | }
124 | msg := color.New(color.FgMagenta).Add(color.Bold).Sprintf("%v. %v", l.stepNumber, message)
125 | l.print.Println(msg)
126 | }
127 |
128 | func (l *Logger) SetLogLevel(level LogLevel) {
129 | l.level = level
130 | }
131 |
132 | // Call this when the colors have been enabled or disabled.
133 | func (l *Logger) RefreshColorPrefixes() {
134 | green := color.New(color.FgGreen)
135 | boldYellow := color.New(color.FgYellow).Add(color.Bold)
136 | boldRed := color.New(color.FgRed).Add(color.Bold)
137 |
138 | l.info.SetPrefix(green.Sprint("info: "))
139 | l.warn.SetPrefix(boldYellow.Sprint("warning: "))
140 | l.error.SetPrefix(boldRed.Sprint("error: "))
141 | }
142 |
--------------------------------------------------------------------------------
/internal/settings/completion_test.go:
--------------------------------------------------------------------------------
1 | package settings_test
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | "github.com/nix-community/nixos-cli/internal/settings"
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | type TestCase struct {
12 | Input string
13 | Expected []string
14 | }
15 |
16 | func TestCompleteConfigFlag(t *testing.T) {
17 | testCases := []TestCase{
18 | // Fields tagged with `noset:"true"` should result in no completions
19 | {"aliases", []string{}},
20 | {"ali", []string{}},
21 | {"init.extra_config", []string{}},
22 |
23 | // Fields with a single match to a settable key should add an = at the end.
24 | {"apply.imply_impure_with_tag", []string{"apply.imply_impure_with_tag="}},
25 | {"apply.imp", []string{"apply.imply_impure_with_tag="}},
26 |
27 | // Fields with further nested keys should add a .
28 | {"app", []string{"apply."}},
29 | {"ent", []string{"enter."}},
30 |
31 | // Fields after a . should be underneath the nested option
32 | {"option.", []string{"option.min_score", "option.prettify", "option.debounce_time"}},
33 |
34 | {"apply.use_", []string{"apply.use_nom", "apply.use_git_commit_msg"}},
35 |
36 | // Invalid fields should result in no completions
37 | {"invalid", []string{}},
38 | {"bruh.lmao", []string{}},
39 |
40 | // Boolean field value completion
41 | {"color=", []string{"color=true", "color=false"}},
42 | {"color=t", []string{"color=true"}},
43 | {"color=f", []string{"color=false"}},
44 | {"color=invalid", []string{}},
45 | }
46 |
47 | for _, testCase := range testCases {
48 | actual, _ := settings.CompleteConfigFlag(&cobra.Command{}, []string{}, testCase.Input)
49 |
50 | // Discard completion descriptions.
51 | for i, v := range actual {
52 | actual[i] = stripAfterTab(v)
53 | }
54 |
55 | if !slicesEqual(actual, testCase.Expected) {
56 | t.Errorf("for input '%s': expected %v, got %v", testCase.Input, testCase.Expected, actual)
57 | }
58 | }
59 | }
60 |
61 | func slicesEqual(a []string, b []string) bool {
62 | if len(a) != len(b) {
63 | return false
64 | }
65 |
66 | for i, v := range a {
67 | if v != b[i] {
68 | return false
69 | }
70 | }
71 |
72 | return true
73 | }
74 |
75 | func stripAfterTab(input string) string {
76 | if i := strings.Index(input, "\t"); i > -1 {
77 | return input[:i]
78 | }
79 | return input
80 | }
81 |
--------------------------------------------------------------------------------
/internal/settings/context.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import "context"
4 |
5 | type settincsCtxKeyType string
6 |
7 | const settingsCtxKey settincsCtxKeyType = "settings"
8 |
9 | func WithConfig(ctx context.Context, cfg *Settings) context.Context {
10 | return context.WithValue(ctx, settingsCtxKey, cfg)
11 | }
12 |
13 | func FromContext(ctx context.Context) *Settings {
14 | logger, ok := ctx.Value(settingsCtxKey).(*Settings)
15 | if !ok {
16 | panic("settings not present in context")
17 | }
18 | return logger
19 | }
20 |
--------------------------------------------------------------------------------
/internal/settings/errors.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import "fmt"
4 |
5 | type SettingsErrors []SettingsError
6 |
7 | type SettingsError struct {
8 | Field string
9 | Message string
10 | }
11 |
12 | func (e SettingsError) Error() string {
13 | return fmt.Sprintf("%s: %s", e.Field, e.Message)
14 | }
15 |
--------------------------------------------------------------------------------
/internal/settings/settings_test.go:
--------------------------------------------------------------------------------
1 | package settings_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/nix-community/nixos-cli/internal/settings"
7 | )
8 |
9 | func TestValidateConfig(t *testing.T) {
10 | t.Run("incorrect config fails", func(t *testing.T) {
11 | cfg := &settings.Settings{
12 | Aliases: map[string][]string{
13 | "": {"value1"},
14 | "-bad": {"value2"},
15 | "has space": {"value3"},
16 | "validalias": {"value4"},
17 | "validalias-noentries": {},
18 | },
19 | Option: settings.OptionSettings{
20 | MinScore: 1,
21 | },
22 | }
23 |
24 | errs := cfg.Validate()
25 | if len(errs) != 4 {
26 | t.Errorf("expected 4 errors, got %d", len(errs))
27 | }
28 |
29 | if len(cfg.Aliases) != 1 {
30 | t.Errorf("expected Aliases to have one valid entry, got %v", cfg.Aliases)
31 | }
32 | })
33 |
34 | t.Run("valid config passes", func(t *testing.T) {
35 | cfg := &settings.Settings{
36 | Aliases: map[string][]string{
37 | "validalias": {"value1", "value2"},
38 | },
39 | Option: settings.OptionSettings{
40 | MinScore: 2,
41 | },
42 | }
43 |
44 | errs := cfg.Validate()
45 | if errs != nil {
46 | t.Errorf("expected error slice to be nil, got %d errors", len(errs))
47 | }
48 | })
49 | }
50 |
51 | func TestSetConfigValue(t *testing.T) {
52 | t.Run("Set int field successfully", func(t *testing.T) {
53 | cfg := &settings.Settings{
54 | Option: settings.OptionSettings{
55 | MinScore: 1,
56 | },
57 | }
58 |
59 | err := cfg.SetValue("option.min_score", "4")
60 | if err != nil {
61 | t.Fatalf("expected option.min_score to be set, err = %v", err)
62 | }
63 |
64 | expected := int64(4)
65 | actual := cfg.Option.MinScore
66 |
67 | if expected != actual {
68 | t.Fatalf("expected option.min_score = %v, actual = %v", expected, actual)
69 | }
70 | })
71 |
72 | t.Run("Set string field successfully", func(t *testing.T) {
73 | cfg := &settings.Settings{
74 | ConfigLocation: "/etc/nixos",
75 | }
76 |
77 | err := cfg.SetValue("config_location", "/home/user")
78 | if err != nil {
79 | t.Fatalf("expected config_location to be set, err = %v", err)
80 | }
81 |
82 | expected := "/home/user"
83 | actual := cfg.ConfigLocation
84 |
85 | if expected != actual {
86 | t.Fatalf("expected config_location = %v, actual = %v", expected, actual)
87 | }
88 | })
89 |
90 | t.Run("Set boolean field successfully", func(t *testing.T) {
91 | cfg := &settings.Settings{
92 | Apply: settings.ApplySettings{
93 | ImplyImpureWithTag: true,
94 | },
95 | }
96 |
97 | err := cfg.SetValue("apply.imply_impure_with_tag", "true")
98 | if err != nil {
99 | t.Fatalf("expected apply.imply_impure_with_tag to be set, err = %v", err)
100 | }
101 |
102 | expected := true
103 | actual := cfg.Apply.ImplyImpureWithTag
104 |
105 | if expected != actual {
106 | t.Fatalf("expected apply.imply_impure_with_tag = %v, actual = %v", expected, actual)
107 | }
108 | })
109 |
110 | t.Run("Invalid key", func(t *testing.T) {
111 | cfg := &settings.Settings{}
112 |
113 | err := cfg.SetValue("invalid_key", "")
114 | if err == nil {
115 | t.Fatalf("expected invalid_key to error out, no errors detected")
116 | }
117 | })
118 |
119 | t.Run("Invalid nested key", func(t *testing.T) {
120 | cfg := &settings.Settings{}
121 |
122 | err := cfg.SetValue("apply.invalid.nested", "")
123 | if err == nil {
124 | t.Fatalf("expected apply.invalid.nested to error out, no errors detected")
125 | }
126 | })
127 |
128 | t.Run("Invalid boolean value", func(t *testing.T) {
129 | cfg := &settings.Settings{
130 | Apply: settings.ApplySettings{
131 | ImplyImpureWithTag: true,
132 | },
133 | }
134 |
135 | err := cfg.SetValue("apply.imply_impure_with_tag", "invalid")
136 | if err == nil {
137 | t.Fatalf("expected apply.imply_impure_with_tag to error out, no errors detected")
138 | }
139 | })
140 |
141 | t.Run("Invalid int value", func(t *testing.T) {
142 | cfg := &settings.Settings{
143 | Option: settings.OptionSettings{
144 | MinScore: 1,
145 | },
146 | }
147 |
148 | err := cfg.SetValue("option.min_score", "invalid")
149 | if err == nil {
150 | t.Fatalf("expected option.min_score to error out, no errors detected")
151 | }
152 |
153 | expected := int64(1)
154 | actual := cfg.Option.MinScore
155 |
156 | if expected != actual {
157 | t.Fatalf("expected option.min_score to remain unchanged, expected = %v actual = %v", expected, actual)
158 | }
159 | })
160 | }
161 |
--------------------------------------------------------------------------------
/internal/system/local.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 |
7 | "github.com/nix-community/nixos-cli/internal/logger"
8 | )
9 |
10 | type LocalSystem struct {
11 | logger *logger.Logger
12 | }
13 |
14 | func NewLocalSystem(logger *logger.Logger) *LocalSystem {
15 | return &LocalSystem{
16 | logger: logger,
17 | }
18 | }
19 |
20 | func (l *LocalSystem) Run(cmd *Command) (int, error) {
21 | command := exec.Command(cmd.Name, cmd.Args...)
22 |
23 | command.Stdout = cmd.Stdout
24 | command.Stderr = cmd.Stderr
25 | command.Stdin = cmd.Stdin
26 | command.Env = os.Environ()
27 |
28 | for key, value := range cmd.Env {
29 | command.Env = append(command.Env, key+"="+value)
30 | }
31 |
32 | err := command.Run()
33 |
34 | if exitErr, ok := err.(*exec.ExitError); ok {
35 | if status, ok := exitErr.Sys().(interface{ ExitStatus() int }); ok {
36 | return status.ExitStatus(), err
37 | }
38 | }
39 |
40 | if err == nil {
41 | return 0, nil
42 | }
43 |
44 | return 0, err
45 | }
46 |
47 | func (l *LocalSystem) IsNixOS() bool {
48 | _, err := os.Stat("/etc/NIXOS")
49 | return err == nil
50 | }
51 |
52 | func (l *LocalSystem) Logger() *logger.Logger {
53 | return l.logger
54 | }
55 |
--------------------------------------------------------------------------------
/internal/system/runner.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "io"
5 | "os"
6 |
7 | "github.com/nix-community/nixos-cli/internal/logger"
8 | )
9 |
10 | type CommandRunner interface {
11 | Run(cmd *Command) (int, error)
12 | Logger() *logger.Logger
13 | }
14 |
15 | type Command struct {
16 | Name string
17 | Args []string
18 | Stdin io.Reader
19 | Stdout io.Writer
20 | Stderr io.Writer
21 | Env map[string]string
22 | }
23 |
24 | func NewCommand(name string, args ...string) *Command {
25 | return &Command{
26 | Name: name,
27 | Args: args,
28 | Stdin: os.Stdin,
29 | Stdout: os.Stdout,
30 | Stderr: os.Stderr,
31 | Env: make(map[string]string),
32 | }
33 | }
34 |
35 | func (c *Command) SetEnv(key string, value string) {
36 | c.Env[key] = value
37 | }
38 |
--------------------------------------------------------------------------------
/internal/system/system.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | type System interface {
4 | IsNixOS() bool
5 | }
6 |
--------------------------------------------------------------------------------
/internal/time/systemd.go:
--------------------------------------------------------------------------------
1 | package time
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "time"
7 | "unicode"
8 | )
9 |
10 | // Parse a time.Duration from a systemd.time(7) string.
11 | func DurationFromTimeSpan(span string) (time.Duration, error) {
12 | if len(span) < 2 {
13 | return 0, fmt.Errorf("time span too short")
14 | }
15 |
16 | for _, c := range span {
17 | if !(unicode.IsDigit(c) || unicode.IsLetter(c) || c == ' ') {
18 | return 0, fmt.Errorf("invalid character %v", c)
19 | }
20 | }
21 |
22 | if !unicode.IsDigit(rune(span[0])) {
23 | return 0, fmt.Errorf("span must start with number")
24 | }
25 |
26 | totalDuration := time.Duration(0)
27 |
28 | i := 0
29 | spanLen := len(span)
30 |
31 | for i < spanLen {
32 | if span[i] == ' ' {
33 | i += 1
34 | continue
35 | }
36 | if !unicode.IsDigit(rune(span[i])) {
37 | return 0, fmt.Errorf("span components must start with numbers")
38 | }
39 |
40 | numStart := i
41 | for i < spanLen && unicode.IsDigit(rune(span[i])) {
42 | i += 1
43 | }
44 | num, _ := strconv.ParseInt(span[numStart:i], 10, 64)
45 |
46 | if i >= spanLen {
47 | return 0, fmt.Errorf("span components must have units")
48 | }
49 |
50 | for unicode.IsSpace(rune(span[i])) {
51 | i += 1
52 | }
53 |
54 | unitStart := i
55 | for i < spanLen && unicode.IsLetter(rune(span[i])) {
56 | i += 1
57 | }
58 | unit := span[unitStart:i]
59 |
60 | var durationUnit time.Duration
61 | if containsSlice(unit, []string{"ns", "nsec"}) {
62 | durationUnit = time.Nanosecond
63 | } else if containsSlice(unit, []string{"us", "usec"}) {
64 | durationUnit = time.Microsecond
65 | } else if containsSlice(unit, []string{"ms", "msec"}) {
66 | durationUnit = time.Millisecond
67 | } else if containsSlice(unit, []string{"s", "sec", "second", "seconds"}) {
68 | durationUnit = time.Second
69 | } else if containsSlice(unit, []string{"m", "min", "minute", "minutes"}) {
70 | durationUnit = time.Minute
71 | } else if containsSlice(unit, []string{"h", "hr", "hour", "hours"}) {
72 | durationUnit = time.Hour
73 | } else if containsSlice(unit, []string{"d", "day", "days"}) {
74 | durationUnit = time.Hour * 24
75 | } else if containsSlice(unit, []string{"w", "week", "weeks"}) {
76 | durationUnit = time.Hour * 24 * 7
77 | } else if containsSlice(unit, []string{"M", "month", "months"}) {
78 | durationUnit = time.Duration(30.44 * float64(24) * float64(time.Hour))
79 | } else if containsSlice(unit, []string{"y", "year", "years"}) {
80 | durationUnit = time.Duration(365.25 * float64(24) * float64(time.Hour))
81 | } else {
82 | return 0, fmt.Errorf("invalid unit")
83 | }
84 |
85 | totalDuration += time.Duration(num) * durationUnit
86 | }
87 |
88 | return totalDuration, nil
89 | }
90 |
91 | func containsSlice(candidate string, candidates []string) bool {
92 | for _, v := range candidates {
93 | if v == candidate {
94 | return true
95 | }
96 | }
97 | return false
98 | }
99 |
--------------------------------------------------------------------------------
/internal/time/systemd_test.go:
--------------------------------------------------------------------------------
1 | package time
2 |
3 | import (
4 | "math"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func durationsApproxEqual(d1, d2, tolerance time.Duration) bool {
10 | diff := d1 - d2
11 | return math.Abs(float64(diff)) <= float64(tolerance)
12 | }
13 |
14 | func TestDurationFromTimeSpan(t *testing.T) {
15 | const tolerance = time.Millisecond
16 |
17 | tests := []struct {
18 | span string
19 | expected time.Duration
20 | expectErr bool
21 | }{
22 | {"1s", time.Second, false},
23 | {"1second", time.Second, false},
24 | {"2m", 2 * time.Minute, false},
25 | {"2min", 2 * time.Minute, false},
26 | {"3h", 3 * time.Hour, false},
27 | {"3hours", 3 * time.Hour, false},
28 | {"1d", 24 * time.Hour, false},
29 | {"1day", 24 * time.Hour, false},
30 | {"1w", 7 * 24 * time.Hour, false},
31 | {"10weeks", 10 * 7 * 24 * time.Hour, false},
32 | {"1h30m", 90 * time.Minute, false},
33 | {"2d3h45m", 2*24*time.Hour + 3*time.Hour + 45*time.Minute, false},
34 | {"0s", 0, false},
35 |
36 | {"", 0, true},
37 | {"1x", 0, true},
38 | {"hour", 0, true},
39 | {"5 10d", 0, true},
40 | }
41 |
42 | for _, tt := range tests {
43 | t.Run(tt.span, func(t *testing.T) {
44 | actual, err := DurationFromTimeSpan(tt.span)
45 |
46 | if (err != nil) != tt.expectErr {
47 | t.Errorf("DurationFromTimeSpan(%q) error = %v, expectErr %v", tt.span, err, tt.expectErr)
48 | return
49 | }
50 |
51 | if !tt.expectErr && !durationsApproxEqual(actual, tt.expected, tolerance) {
52 | t.Errorf("DurationFromTimeSpan(%q) = %v, expected ~%v", tt.span, actual, tt.expected)
53 | }
54 | })
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/internal/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 | "strings"
8 | "syscall"
9 | )
10 |
11 | // Re-exec the current process as root with the same arguments.
12 | // This is done with the provided rootCommand parameter, which
13 | // usually is "sudo" or "doas", and comes from the command config.
14 | func ExecAsRoot(rootCommand string) error {
15 | rootCommandPath, err := exec.LookPath(rootCommand)
16 | if err != nil {
17 | return err
18 | }
19 |
20 | argv := []string{rootCommand}
21 | argv = append(argv, os.Args...)
22 |
23 | err = syscall.Exec(rootCommandPath, argv, os.Environ())
24 | return err
25 | }
26 |
27 | func EscapeAndJoinArgs(args []string) string {
28 | var escapedArgs []string
29 |
30 | for _, arg := range args {
31 | if strings.ContainsAny(arg, " \t\n\"'\\") {
32 | arg = strings.ReplaceAll(arg, "\\", "\\\\")
33 | arg = strings.ReplaceAll(arg, "\"", "\\\"")
34 | escapedArgs = append(escapedArgs, fmt.Sprintf("\"%s\"", arg))
35 | } else {
36 | escapedArgs = append(escapedArgs, arg)
37 | }
38 | }
39 |
40 | return strings.Join(escapedArgs, " ")
41 | }
42 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/nix-community/nixos-cli/cmd/root"
4 |
5 | func main() {
6 | root.Execute()
7 | }
8 |
--------------------------------------------------------------------------------
/module.nix:
--------------------------------------------------------------------------------
1 | self: {
2 | options,
3 | config,
4 | pkgs,
5 | lib,
6 | ...
7 | }: let
8 | cfg = config.services.nixos-cli;
9 | nixosCfg = config.system.nixos;
10 |
11 | inherit (lib) types;
12 |
13 | tomlFormat = pkgs.formats.toml {};
14 | in {
15 | options.services.nixos-cli = {
16 | enable = lib.mkEnableOption "unified NixOS tooling replacement for nixos-* utilities";
17 |
18 | package = lib.mkOption {
19 | type = types.package;
20 | default = self.packages.${pkgs.system}.nixos;
21 | description = "Package to use for nixos-cli";
22 | };
23 |
24 | config = lib.mkOption {
25 | type = tomlFormat.type;
26 | default = {};
27 | description = "Configuration for nixos-cli, in TOML format";
28 | apply = prev: let
29 | # Inherit this from the old nixos-generate-config attrs. Easy to deal with, for now.
30 | desktopConfig = lib.concatStringsSep "\n" config.system.nixos-generate-config.desktopConfiguration;
31 | in
32 | lib.recursiveUpdate {
33 | init = {
34 | xserver_enabled = config.services.xserver.enable;
35 | desktop_config = desktopConfig;
36 | extra_config = "";
37 | };
38 | }
39 | prev;
40 | };
41 |
42 | generationTag = lib.mkOption {
43 | type = types.nullOr types.str;
44 | default = lib.maybeEnv "NIXOS_GENERATION_TAG" null;
45 | description = "A description for this generation";
46 | example = "Sign Git GPG commits by default";
47 | };
48 |
49 | prebuildOptionCache = lib.mkOption {
50 | type = types.bool;
51 | default = config.documentation.nixos.enable;
52 | description = "Prebuild JSON cache for `nixos option` command";
53 | };
54 | };
55 |
56 | config = lib.mkIf cfg.enable {
57 | environment.systemPackages = [cfg.package];
58 |
59 | # While there is already an `options.json` that exists in the
60 | # `config.system.build.manual.optionsJSON` attribute, this is
61 | # not as full-featured, because it does not contain NixOS options
62 | # that are not available in base `nixpkgs`. This does increase
63 | # eval time, but that's a fine tradeoff in this case since it
64 | # is able to be disabled.
65 | environment.etc."nixos-cli/options-cache.json" = lib.mkIf cfg.prebuildOptionCache {
66 | text = let
67 | optionList' = lib.optionAttrSetToDocList options;
68 | optionList = builtins.filter (v: v.visible && !v.internal) optionList';
69 | in
70 | builtins.toJSON optionList;
71 | };
72 |
73 | environment.etc."nixos-cli/config.toml".source =
74 | tomlFormat.generate "nixos-cli-config.toml" cfg.config;
75 |
76 | # Hijack system builder commands to insert a `nixos-version.json` file at the root.
77 | system.systemBuilderCommands = let
78 | nixos-version-json = builtins.toJSON {
79 | nixosVersion = "${nixosCfg.distroName} ${nixosCfg.release} (${nixosCfg.codeName})";
80 | nixpkgsRevision = nixosCfg.revision;
81 | configurationRevision = "${builtins.toString config.system.configurationRevision}";
82 | description = cfg.generationTag;
83 | };
84 | in ''
85 | cat > "$out/nixos-version.json" << EOF
86 | ${nixos-version-json}
87 | EOF
88 | '';
89 |
90 | security.sudo.extraConfig = ''
91 | # Preserve NIXOS_CONFIG and NIXOS_CLI_CONFIG in sudo invocations of
92 | # `nixos apply`. This is required in order to keep ownership across
93 | # automatic re-exec as root.
94 | Defaults env_keep += "NIXOS_CONFIG"
95 | Defaults env_keep += "NIXOS_CLI_CONFIG"
96 | '';
97 | };
98 | }
99 |
--------------------------------------------------------------------------------
/package.nix:
--------------------------------------------------------------------------------
1 | {
2 | lib,
3 | buildGoModule,
4 | nix-gitignore,
5 | installShellFiles,
6 | stdenv,
7 | scdoc,
8 | revision ? "unknown",
9 | flake ? true,
10 | }:
11 | buildGoModule (finalAttrs: {
12 | pname = "nixos-cli";
13 | version = "0.12.2-dev";
14 | src = nix-gitignore.gitignoreSource [] ./.;
15 |
16 | vendorHash = "sha256-mW9nsQdNpaKa7E+KvjQLxpt3aGxqThxap4XSI77xCvg=";
17 |
18 | nativeBuildInputs = [installShellFiles scdoc];
19 |
20 | env = {
21 | CGO_ENABLED = 0;
22 | COMMIT_HASH = revision;
23 | FLAKE = lib.boolToString flake;
24 | VERSION = finalAttrs.version;
25 | };
26 |
27 | buildPhase = ''
28 | runHook preBuild
29 | make all gen-manpages
30 | runHook postBuild
31 | '';
32 |
33 | installPhase = ''
34 | runHook preInstall
35 |
36 | install -Dm755 ./nixos -t $out/bin
37 |
38 | mkdir -p $out/share/man/man1
39 | mkdir -p $out/share/man/man5
40 | find man -name '*.1' -exec cp {} $out/share/man/man1/ \;
41 | find man -name '*.5' -exec cp {} $out/share/man/man5/ \;
42 |
43 | runHook postInstall
44 | '';
45 |
46 | postInstall = lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) ''
47 | installShellCompletion --cmd nixos \
48 | --bash <($out/bin/nixos completion bash) \
49 | --fish <($out/bin/nixos completion fish) \
50 | --zsh <($out/bin/nixos completion zsh)
51 | '';
52 |
53 | meta = with lib; {
54 | homepage = "https://github.com/nix-community/nixos";
55 | description = "A unified NixOS tooling replacement for nixos-* utilities";
56 | license = licenses.gpl3Only;
57 | maintainers = with maintainers; [water-sucks];
58 | };
59 | })
60 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | (
2 | import
3 | (
4 | let
5 | lock = builtins.fromJSON (builtins.readFile ./flake.lock);
6 | in
7 | fetchTarball {
8 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
9 | sha256 = lock.nodes.flake-compat.locked.narHash;
10 | }
11 | )
12 | {src = ./.;}
13 | )
14 | .shellNix
15 |
--------------------------------------------------------------------------------
]