├── .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 | --------------------------------------------------------------------------------