Skip to content

feat: create self-contained activation command#94

Merged
water-sucks merged 34 commits intonix-community:mainfrom
water-sucks:activation
Oct 20, 2025
Merged

feat: create self-contained activation command#94
water-sucks merged 34 commits intonix-community:mainfrom
water-sucks:activation

Conversation

@water-sucks
Copy link
Copy Markdown
Collaborator

@water-sucks water-sucks commented Jul 27, 2025

Description

This is a re-implementation of the switch-to-configuration script in Go. Originally, it was implemented a separate script in Perl, but now is a standalone Rust application.

Both are not suitable for the purposes of a self-contained binary, so this is the same functionality, exposed slightly differently.

It is exposed as a low-level command in nixos activate, and generally must be ran in the context of the corresponding NixOS module that uses it as the active switch application, similar to how the enableNg option was used for the Rust application.

Running nixos activate as a user directly serves as a shorthand for executing the /run/current-system/bin/switch-to-configuration script instead, which will either wrap a proper invocation of nixos activate with the required variables, or run the original nixpkgs implementation of it in Rust.

Closes #55.

Summary by CodeRabbit

  • New Features
    • Adds an activate command (boot, check, dry-activate, switch, test) with dynamic completion, contextual help, conditional specialisation flag, Linux-only activation flow (dry-run supported), user-mode switch, unit lifecycle management, unit-file comparison, and filesystem/swap handling.
  • Documentation
    • New manual page for activate; expanded environment and development-mode docs and env var controls for activation behavior.
  • Tests
    • Unit tests for unit-file comparison logic.
  • Chores
    • Toolchain/module updates and package metadata changes; new module option to opt into activation-based workflow.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 20, 2025

Walkthrough

Adds a new "nixos activate" command and Linux-only activation implementation: CLI wiring and option parsing, activation runtime (locking, pre-checks, bootloader, interface validation), unit/file parsing and classification, user re-exec path, docs, module wiring, and dependency updates.

Changes

Cohort / File(s) Summary
Activation CLI
cmd/activate/activate.go, cmd/root/root.go, internal/cmd/opts/opts.go, internal/cmd/utils/utils.go
New ActivateCommand() integrated into root; ActivateOpts added; argument parsing and validation with ParseSwitchToConfigurationAction; dynamic completion; conditional --specialisation flag; help formatting via AlignedOptions; RunE delegates to activateMain.
Activation Runtime (Linux)
cmd/activate/run.go, cmd/activate/user.go, cmd/activate/fs.go, cmd/activate/unit.go
Linux-only activation flow: RequiredVars and ErrorRequiredVarMissing types, ErrMismatchedInterfaceVersion, activation locking and orchestration, environment extraction, pre-switch checks, bootloader handling, locale/runtime handling, fstab parsing and swapoff syscall, user re-exec and systemd user restart, unit classification/resolution and action-list generation (Start/Stop/Restart/Reload/Skip/Filter).
Non-Linux Unsupported Path
cmd/activate/unsupported.go
Non-Linux build provides activateMain that logs and returns an unsupported-on-non-NixOS error.
Activation Internals & Env
internal/activation/activation.go, internal/system/local.go
Added ParseSwitchToConfigurationAction and new ChecksOnly enum value; set NIXOS_CLI_ATTEMPTING_ACTIVATION env during SwitchToConfiguration; IsNixOS extended to parse /etc/os-release.
systemd Unit Parsing & Comparison
internal/systemd/unit.go, internal/systemd/unit_test.go, internal/systemd/time.go, internal/systemd/time_test.go
New UnitInfo type, ParseUnit (base + .d drop-ins), property accessors, ParseBool, CompareUnits (Equal/NeedsReload/NeedsRestart) with ignore rules; added tests; package renamed to systemdUtils.
Activation Documentation & Env Docs
doc/man/nixos-cli-activate.1.scd, doc/man/nixos-cli-env.5.scd
Manual page for nixos activate added; environment docs updated with NIXOS_NO_CHECK, NIXOS_NO_SYNC, STC_DISPLAY_ALL_UNITS, and development-mode variables including NIXOS_CLI_ATTEMPTING_ACTIVATION.
NixOS Module & Packaging
module.nix, package.nix
New useActivationInterface option and alternative activation wiring; meta.mainProgram = "nixos" added; vendorHash updated.
Go Modules & Dependencies
go.mod
Bumps Go toolchain to 1.25; adds carapace, google/shlex, optnix, golang.org/x/sys; replaces coreos/go-systemd/v22 with water-sucks/go-systemd/v22; adjusts indirect dependencies.
Import Migration (duration parsing)
cmd/generation/delete/delete.go, cmd/generation/delete/resolver.go
Replaced imports from internal/time to internal/systemd and switched calls to systemdUtils.DurationFromTimeSpan for OlderThan parsing.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant U as User
    participant CLI as "nixos activate"
    participant Env as Env/Validation
    participant Lock as Activation Lock
    participant DBus as systemd/logind (DBus)
    participant Units as Unit Manager
    participant Script as Activation Scripts

    U->>CLI: run `nixos activate ACTION`
    CLI->>Env: parse ACTION & required vars
    Env->>Lock: acquire activation lock
    alt unsupported (non-Linux)
        CLI-->>U: error (unsupported)
    else supported (Linux)
        Lock-->>CLI: lock acquired
        CLI->>DBus: init system DBus
        CLI->>Script: run pre-switch checks & install bootloader
        alt dry/check mode
            CLI->>Units: classify units (read-only)
            Units-->>CLI: planned actions
            CLI-->>U: display plan
        else real switch
            CLI->>Units: classify modified & resolve templated units
            Units-->>CLI: action lists
            CLI->>DBus: stop/reload/restart/start units
            DBus-->>CLI: action results
            CLI->>Script: run activation scripts (post-activation)
            CLI-->>U: activation result
        end
        CLI->>Lock: release lock
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat: create self-contained activation command" accurately and concisely describes the primary change introduced in this pull request. The changeset clearly implements a new standalone nixos activate command system through multiple new files in cmd/activate/, integration into the root command, documentation, and module configuration. The title is specific enough to convey the main objective without being verbose or vague.

[pre_merge_check_pass]

The PR comprehensively addresses all coding-related requirements from issue #55. It implements the new nixos activate command (#55) with complete activation workflow logic including environment variable handling, pre-switch checks, bootloader installation, unit management, and filesystem operations. The NixOS module integration (#55) is provided via module.nix with the useActivationInterface option. Supporting infrastructure includes action parsing in internal/activation/activation.go, systemd utilities in internal/systemd/unit.go, command options in internal/cmd/opts/opts.go, and proper documentation. All key requirements for behavioral parity, generation context execution, and modular support are addressed through the implementation. |
| Out of Scope Changes Check | ✅ Passed | All changes are directly scoped to implementing the activation command and its supporting infrastructure. The refactoring of time utilities to systemd utilities (internal/systemd/time.go and related files) is a necessary supporting change for the activation system. The systemd unit parsing logic (internal/systemd/unit.go) is used directly in unit management. Infrastructure updates to internal/system/local.go support NixOS detection, and dependency updates in go.mod provide required libraries for systemd interaction, shell completion, and system calls. No unrelated or tangential changes were identified. |

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@water-sucks
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 20, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
internal/activation/activation.go (3)

51-56: Simplify MkdirAll error handling.

os.MkdirAll never returns os.ErrExist; the nested check is misleading. Return errors directly.

- err := os.MkdirAll(constants.NixSystemProfileDirectory, 0o755)
- if err != nil {
-   if err != os.ErrExist {
-     return fmt.Errorf("failed to create nix system profile directory: %w", err)
-   }
- }
+ if err := os.MkdirAll(constants.NixSystemProfileDirectory, 0o755); err != nil {
+   return fmt.Errorf("failed to create nix system profile directory: %w", err)
+ }

61-68: Likely inverted condition for ensuring the system profile directory.

You call EnsureSystemProfileDirectoryExists() when profile != "system". This seems backwards.

- if profile != "system" {
+ if profile == "system" {
     err := EnsureSystemProfileDirectoryExists()
     if err != nil {
       return err
     }
   }

Apply in both AddNewNixProfile and SetNixProfileGeneration.

Also applies to: 84-91


119-129: Avoid panic on unexpected generation link format.

Return an error instead of panicking to prevent crashing callers.

- } else {
-   panic("current link format does not match 'profile-generation-link' format")
- }
+ }
+ return 0, fmt.Errorf("current link format does not match 'profile-generation-link' format")
🧹 Nitpick comments (17)
internal/cmd/utils/utils.go (1)

48-68: Consider using strings.Builder for better performance.

The current implementation concatenates strings in a loop (line 64), which creates multiple string allocations. While this is acceptable for small option maps (help text), using strings.Builder would be more efficient.

Apply this diff to improve performance:

 func AlignedOptions(options map[string]string) string {
 	maxLen := 0
 	for cmd := range options {
 		if len(cmd) > maxLen {
 			maxLen = len(cmd)
 		}
 	}
 
-	result := ""
+	var result strings.Builder
 	format := fmt.Sprintf("  %%-%ds  %%s\n", maxLen)
 
 	keys := slices.Collect(maps.Keys(options))
 	sort.Strings(keys)
 
 	for _, cmd := range keys {
 		desc := options[cmd]
-		result += fmt.Sprintf(format, cmd, desc)
+		result.WriteString(fmt.Sprintf(format, cmd, desc))
 	}
 
-	return result
+	return result.String()
 }
cmd/activate/user.go (1)

15-39: Consider attaching stdin to the child process.

Lines 35-36 attach Stdout and Stderr, but Stdin is not attached. If the re-executed process needs to read from stdin (e.g., for confirmation prompts), it will fail.

Apply this diff to attach stdin:

 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
+	cmd.Stdin = os.Stdin
 
 	return cmd.Run()
cmd/activate/fs.go (1)

29-74: Consider logging malformed fstab entries.

The parseFstab function silently skips malformed lines (lines with fewer than 3 fields). While this is lenient and prevents activation failures due to minor fstab issues, it could also hide configuration errors that users should be aware of.

Consider adding optional debug logging for skipped lines:

 		fields := strings.Fields(line)
 
 		if len(fields) < 3 {
+			// Optionally log: "skipping malformed fstab line: %s", line
 			continue
 		}

This would help users diagnose why expected filesystems or swap devices aren't being processed, especially during troubleshooting.

cmd/activate/activate.go (2)

28-45: Tighten arg validation (reject extra positional args).

Currently only checks for presence of ACTION. Enforce exactly one positional arg when not under parent exe to avoid accepting unintended args.

Apply:

-        Args: func(cmd *cobra.Command, args []string) error {
+        Args: func(cmd *cobra.Command, args []string) error {
           if os.Getenv(NIXOS_STC_PARENT_EXE) != "" {
             return nil
           }
 
-          if len(args) == 0 {
+          if len(args) == 0 {
             return fmt.Errorf("missing required argument <ACTION>")
           }
+          if len(args) > 1 {
+            return fmt.Errorf("too many arguments; expected exactly one <ACTION>")
+          }
 
           a, err := activation.ParseSwitchToConfigurationAction(args[0])
           if err != nil {
             return err
           }
 
           opts.Action = a
 
           return nil
         },

16-22: Deterministic help ordering for actions.

Map iteration is random; help/completion order will vary. Sort keys before assembling results and AlignedOptions.

-   results := make([]string, 0, len(commands))
-   for command, desc := range commands {
-     results = append(results, fmt.Sprintf("%v\t%v", command, desc))
-   }
+   keys := make([]string, 0, len(commands))
+   for k := range commands { keys = append(keys, k) }
+   sort.Strings(keys)
+   results := make([]string, 0, len(keys))
+   for _, command := range keys {
+     results = append(results, fmt.Sprintf("%v\t%v", command, commands[command]))
+   }
- cmd.SetHelpTemplate(cmd.HelpTemplate() + "\nActions:\n" + cmdUtils.AlignedOptions(commands))
+ cmd.SetHelpTemplate(cmd.HelpTemplate() + "\nActions:\n" + cmdUtils.AlignedOptions(cmdUtils.SortOptions(commands)))

(Assumes a SortOptions helper; otherwise sort locally before formatting.)

Also applies to: 47-53, 63-65

internal/activation/activation.go (1)

159-174: Prefer returning "unknown" over panicking in String().

Panics in String() make diagnostics harder. Consider returning "unknown".

- default:
-   panic("unknown switch to configuration action type")
+ default:
+   return "unknown"
module.nix (2)

48-66: Polish option docs and line breaks.

Minor grammar/formatting fixes; avoid breaking a sentence across lines “switchable. system.”.

-        own, but it will serve solely as a shim to run the switch script on a switchable.
-        system.
+        own, but it will serve solely as a shim to run the switch script on a
+        switchable system.

144-154: Ensure wrapper works with paths containing spaces and missing vars.

Even if unlikely in Nix store, quote $out and guard unset variables (like toplevelVar). Also consider exporting NIXOS_ACTION in wrapper for parity.

-          mkdir $out/bin
+          mkdir -p "$out/bin"
-          ln -sf ${lib.getExe cfg.package} $out/bin/switch-to-configuration
+          ln -sf ${lib.getExe cfg.package} "$out/bin/switch-to-configuration"
-          wrapProgram $out/bin/switch-to-configuration \
+          wrapProgram "$out/bin/switch-to-configuration" \
             --add-flags activate \
             --set NIXOS_CLI_ATTEMPTING_ACTIVATION 1 \
-            --set OUT $out \
-            --set TOPLEVEL ''${!toplevelVar} \
+            --set OUT "$out" \
+            --set TOPLEVEL ''${!toplevelVar:-} \
cmd/activate/run.go (2)

171-186: Handle missing init-interface-version files gracefully.

Older generations may not have this file. Treat missing files as mismatch (exit 100) without printing a read error, or warn and continue per intended compatibility.

- currentInitInterfaceVersion, err := os.ReadFile(currentInitInterfaceVersionFile)
- if err != nil {
-   return fmt.Errorf("failed to read %s: %v", currentInitInterfaceVersionFile, err)
- }
+ currentInitInterfaceVersion, err := os.ReadFile(currentInitInterfaceVersionFile)
+ if err != nil {
+   if errors.Is(err, os.ErrNotExist) {
+     return ErrMismatchedInterfaceVersion
+   }
+   return fmt.Errorf("failed to read %s: %w", currentInitInterfaceVersionFile, err)
+ }
 
- newInitInterfaceVersion, err := os.ReadFile(newInitInterfaceVersionFile)
- if err != nil {
-   return fmt.Errorf("failed to read %s: %v", newInitInterfaceVersionFile, err)
- }
+ newInitInterfaceVersion, err := os.ReadFile(newInitInterfaceVersionFile)
+ if err != nil {
+   if errors.Is(err, os.ErrNotExist) {
+     return ErrMismatchedInterfaceVersion
+   }
+   return fmt.Errorf("failed to read %s: %w", newInitInterfaceVersionFile, err)
+ }

And keep the os.Exit(100) branch as-is for a clear user-facing outcome.

Also applies to: 431-437


331-335: Nit: wording.

“This command must be ran as root” → “run”.

- err := fmt.Errorf("this command must be ran as root")
+ err := fmt.Errorf("this command must be run as root")
internal/systemd/unit.go (3)

125-133: Make ParseBool robust to common forms.

Support 0/1 and mixed/upper case to match systemd’s lenient booleans.

-func ParseBool(value string) bool {
-  switch value {
-  case "1", "yes", "true", "on":
-    return true
-  default:
-    return false
-  }
-}
+func ParseBool(value string) bool {
+  v := strings.TrimSpace(strings.ToLower(value))
+  switch v {
+  case "1", "yes", "true", "on":
+    return true
+  default:
+    return false
+  }
+}

(Remember to import "strings".)


20-58: Consider parsing override unit file itself (not only .d).

When unitFilePath differs from baseUnitFilePath, you parse only its .d/*.conf and not the unitFilePath file. If that path can be a complete override file, include it.

 if unitFilePath != baseUnitFilePath {
-  if _, err := os.Stat(unitFilePath); err == nil {
+  if _, err := os.Stat(unitFilePath); err == nil {
+    if err := parseAndMerge(unitFilePath); err != nil { return nil, err }
     matches, _ := filepath.Glob(fmt.Sprintf("%s.d/*.conf", unitFilePath))
     for _, path := range matches {
       _ = parseAndMerge(path)
     }
   }
 }

60-82: Doc nit: complete the comment.

Finish the “Consider using …” sentence or remove it to avoid confusion.

cmd/activate/unit.go (4)

192-200: Sorted() mutates case of unit names in output.

Lowercasing before returning changes values. Sort case-insensitively but return originals.

Apply:

- keys := make([]string, 0, len(l))
- for unit := range l {
-   keys = append(keys, strings.ToLower(unit))
- }
- sort.Strings(keys)
- return keys
+ keys := make([]string, 0, len(l))
+ for unit := range l {
+   keys = append(keys, unit)
+ }
+ sort.Slice(keys, func(i, j int) bool {
+   a, b := strings.ToLower(keys[i]), strings.ToLower(keys[j])
+   if a == b {
+     return keys[i] < keys[j]
+   }
+   return a < b
+ })
+ return keys

27-29: Template regex may reject valid instances with dots; prefer last-dot split for extension.

Instances can contain dots; current [^\.]* disallows them. Use a pattern that captures up to the last dot as the extension while allowing dots in the instance.

Apply:

- templateUnitPattern = regexp.MustCompile(`^(.*)@([^\.]*)\.(.*)$`)
+ // <name>@<instance>.<ext> — allow dots in <instance>, ext has no dots
+ templateUnitPattern = regexp.MustCompile(`^([^@]+)@(.+)\.([^.]+)$`)

493-505: resolveUnit only falls back to base when both instance files are missing.

If either current or new instance file exists but the other doesn’t, we still want base resolution for templated units. Otherwise CurrentBaseFile may point to a non-existent instance file and upstream checks will skip active templated units.

Apply:

- if matches := templateUnitPattern.FindStringSubmatch(unit); len(matches) > 3 {
-   templateName, _, unitType := matches[1], matches[2], matches[3]
-
-   if _, err := os.Stat(currentUnitFile); os.IsNotExist(err) {
-     if _, err := os.Stat(newUnitFile); os.IsNotExist(err) {
-       baseUnit = fmt.Sprintf("%s@.%s", templateName, unitType)
-       currentBaseFile = filepath.Join("/etc/systemd/system", baseUnit)
-       newBaseFile = filepath.Join(toplevel, "etc/systemd/system", baseUnit)
-     }
-   }
- }
+ if matches := templateUnitPattern.FindStringSubmatch(unit); len(matches) > 3 {
+   templateName, _, unitType := matches[1], matches[2], matches[3]
+   // Prefer base unit for templates unless both instance files are present.
+   if _, err := os.Stat(currentUnitFile); os.IsNotExist(err) {
+     baseUnit = fmt.Sprintf("%s@.%s", templateName, unitType)
+   } else if _, err := os.Stat(newUnitFile); os.IsNotExist(err) {
+     baseUnit = fmt.Sprintf("%s@.%s", templateName, unitType)
+   }
+   if baseUnit != unit {
+     currentBaseFile = filepath.Join("/etc/systemd/system", baseUnit)
+     newBaseFile = filepath.Join(toplevel, "etc/systemd/system", baseUnit)
+   }
+ }

554-569: Make list-file writes idempotent to avoid duplicate entries.

recordUnit can append the same unit multiple times; unrecordUnit then rewrites the whole file. Consider de-duping on write for stability and smaller files.

Example approach:

  • Keep an in-memory recorded set per file to skip duplicates; or
  • Read once and append only if not present; or
  • Write list files from the in-memory UnitList at the end of classification.

Also applies to: 595-596

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8f5ce1 and 036b860.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (22)
  • cmd/activate/activate.go (1 hunks)
  • cmd/activate/fs.go (1 hunks)
  • cmd/activate/run.go (1 hunks)
  • cmd/activate/unit.go (1 hunks)
  • cmd/activate/unsupported.go (1 hunks)
  • cmd/activate/user.go (1 hunks)
  • cmd/generation/delete/delete.go (2 hunks)
  • cmd/generation/delete/resolver.go (2 hunks)
  • cmd/root/root.go (2 hunks)
  • doc/man/nixos-cli-activate.1.scd (1 hunks)
  • doc/man/nixos-cli-env.5.scd (1 hunks)
  • go.mod (4 hunks)
  • internal/activation/activation.go (2 hunks)
  • internal/cmd/opts/opts.go (1 hunks)
  • internal/cmd/utils/utils.go (2 hunks)
  • internal/system/local.go (2 hunks)
  • internal/systemd/time.go (1 hunks)
  • internal/systemd/time_test.go (1 hunks)
  • internal/systemd/unit.go (1 hunks)
  • internal/systemd/unit_test.go (1 hunks)
  • module.nix (2 hunks)
  • package.nix (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build/Test
🔇 Additional comments (18)
package.nix (2)

16-16: LGTM!

The vendorHash update aligns with the new dependencies added in go.mod (coreos/go-systemd, google/shlex, etc.).


59-59: LGTM!

Adding mainProgram metadata is appropriate for packages with a primary executable and helps Nix properly identify the main binary.

internal/system/local.go (2)

50-50: LGTM!

The regex pattern correctly matches the NixOS distro ID with or without quotes, which is appropriate for parsing /etc/os-release values.


52-69: LGTM!

The improved NixOS detection logic correctly prioritizes the canonical /etc/NIXOS marker, then falls back to parsing /etc/os-release. Error handling is appropriate with graceful degradation.

go.mod (3)

6-23: LGTM!

The new dependencies align well with the activation command requirements:

  • carapace: Shell completion support
  • go-systemd: Systemd integration for activation
  • google/shlex: Command-line parsing
  • optnix: Nix options handling
  • golang.org/x/sys: Linux system calls

3-3: Original review comment is based on outdated information.

Go 1.25.3 is the latest stable release as of October 20, 2025, making the go 1.25 specification in go.mod correct and current. The code change requires no modifications.


94-94: Verify the necessity and plan for the go-systemd fork.

The review comment accurately identifies a legitimate concern. Verification confirms:

  • The fork from water-sucks/go-systemd is present in the codebase and actively used (in internal/systemd/unit.go, cmd/activate/user.go, cmd/activate/unit.go, cmd/activate/run.go)
  • The upstream coreos/go-systemd v22.6.0 was recently released on August 20, 2025
  • No documentation or inline comments in go.mod explain why the fork is necessary
  • The fork uses a pseudo-version from October 14, 2025, newer than the upstream release

The fork introduces maintenance and security risks by depending on a custom fork instead of the actively maintained upstream. The original review comment's request for clarification is valid and should be addressed by explaining:

  1. Why the fork is necessary (what features or fixes are missing from upstream)
  2. The plan for contributing changes back upstream
  3. Any security considerations
cmd/generation/delete/resolver.go (1)

12-12: LGTM!

The refactor from timeUtils to systemdUtils is consistent with the package reorganization seen across the PR.

Also applies to: 96-96

cmd/generation/delete/delete.go (1)

24-24: LGTM!

The refactor from the previous time utilities package to systemdUtils is consistent with the package reorganization.

Also applies to: 45-45

internal/systemd/time_test.go (1)

1-1: LGTM!

The package rename to systemdUtils aligns with the refactored time utilities module.

cmd/root/root.go (1)

18-18: LGTM! Clean integration of the new activate command.

The import and command wiring follow the established patterns in this file and properly integrate the new activation functionality into the CLI.

Also applies to: 141-141

internal/cmd/opts/opts.go (1)

3-6: LGTM! Well-structured options type for the activate command.

The new ActivateOpts struct follows the established pattern in this file and properly references the SwitchToConfigurationAction type from the internal activation package.

Also applies to: 13-16

cmd/activate/unsupported.go (1)

1-18: LGTM! Appropriate non-Linux fallback handler.

The build tag ensures this only compiles on non-Linux systems, and the error message clearly communicates that activation is unsupported. The unused action parameter is correctly indicated with the blank identifier.

doc/man/nixos-cli-activate.1.scd (1)

1-65: LGTM! Comprehensive and well-structured manual page.

The documentation clearly explains the command's purpose, available actions, and options. The note about backwards compatibility with switch-to-configuration-ng and the warning that this is a low-level command are helpful context for users.

doc/man/nixos-cli-env.5.scd (1)

93-113: LGTM! Excellent documentation structure for development variables.

Separating development-mode variables into their own section with a clear warning is a good practice. The documentation for the recursion-prevention variables is particularly helpful for maintainers.

internal/systemd/unit_test.go (1)

1-94: LGTM! Well-structured unit tests with good coverage.

The table-driven test structure is idiomatic Go and provides clear coverage of the CompareUnits function's behavior across different scenarios. The test names are descriptive and the assertions provide helpful error messages.

cmd/activate/fs.go (1)

80-91: LGTM! Proper syscall wrapper implementation.

The swapoff function correctly wraps the SYS_SWAPOFF syscall using the appropriate unsafe.Pointer conversion and error handling. This is the standard approach for syscalls not directly exposed by the unix package.

internal/systemd/time.go (1)

1-1: Package rename verification passed.

All references to the old package name have been successfully updated. The ripgrep searches confirmed:

  • No stale references to the old internal/systemd.*time\. pattern remain
  • No old function call syntax (time.DurationFromTimeSpan) remains
  • The new package name is correctly used in all locations (systemdUtils.DurationFromTimeSpan in 2 files)

@water-sucks water-sucks force-pushed the activation branch 2 times, most recently from fbc2e48 to a7a9996 Compare October 20, 2025 06:49
@water-sucks water-sucks marked this pull request as ready for review October 20, 2025 06:50
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (4)
internal/systemd/unit.go (1)

48-55: Critical: ParseUnit still ignores the instance file (unitFilePath).

Only the base file and .d drop-ins are merged; the main unitFilePath is never parsed, so instance-level unit content is lost and comparisons can be wrong.

The code parses baseUnitFilePath (line 39) and its drop-ins (lines 43-46), then parses drop-ins for unitFilePath (lines 50-52), but never parses unitFilePath itself. This causes instance-specific unit settings to be silently ignored, leading to incorrect unit comparisons and potentially failed activations.

Apply this diff to parse the instance file:

 if unitFilePath != baseUnitFilePath {
   if _, err := os.Stat(unitFilePath); err == nil {
+    // Parse the instance file itself before its drop-ins
+    if err := parseAndMerge(unitFilePath); err != nil {
+      return nil, err
+    }
     matches, _ := filepath.Glob(fmt.Sprintf("%s.d/*.conf", unitFilePath))
     for _, path := range matches {
       _ = parseAndMerge(path)
     }
   }
 }
cmd/activate/activate.go (1)

28-31: Undefined identifier on non-Linux builds (use literal or move const).

activate.go references NIXOS_STC_PARENT_EXE but it’s defined only in run.go with a linux build tag, causing compile errors on other platforms. Use the literal here or move the const to a non-OS‑specific file.

-            if os.Getenv(NIXOS_STC_PARENT_EXE) != "" {
+            if os.Getenv("__NIXOS_SWITCH_TO_CONFIGURATION_PARENT_EXE") != "" {
                 return nil
             }
module.nix (1)

102-112: Scope sudo env_keep to the specific command; avoid global leakage.

Defaults env_keep applies to all sudo sessions, expanding attack surface. Restrict to the nixos-cli/switch wrapper path via a Cmnd_Alias and Defaults!cmd form.

Example:

security.sudo.extraConfig = ''
  Cmnd_Alias NIXOS_SWITCH = ${lib.getExe cfg.package} *, ${lib.getExe cfg.package} activate *
  Defaults!${lib.getExe cfg.package} env_keep += "NIXOS_CONFIG NIXOS_GENERATION_TAG NIXOS_CLI_CONFIG NIXOS_CLI_DISABLE_STEPS NIXOS_CLI_DEBUG_MODE NIXOS_CLI_SUPPRESS_NO_SETTINGS_WARNING"
'';

Would you like a PR-ready Nix snippet tailored to your exact wrapper path?

cmd/activate/run.go (1)

117-121: Fix specialisation existence check and error.

Formats wrong value and returns a possibly stale err. Return a new error that includes the requested specialisation.

-        if !slices.Contains(specialisations, specialisation) {
-            err = fmt.Errorf("specialisation '%v' does not exist", specialisations)
-            log.Error(err)
-            return err
-        }
+        if !slices.Contains(specialisations, specialisation) {
+            return fmt.Errorf("specialisation defined in $NIXOS_SPECIALISATION %q does not exist", specialisation)
+        }

Also guard the check behind err == nil to avoid false negatives when listing fails:

-    if specialisation != "" {
-        specialisations, err := generation.CollectSpecialisations(constants.CurrentSystem)
-        if err != nil {
-            log.Warnf("unable to access specialisations: %v", err)
-        }
-        if !slices.Contains(specialisations, specialisation) {
+    if specialisation != "" {
+        specialisations, err := generation.CollectSpecialisations(constants.CurrentSystem)
+        if err == nil && !slices.Contains(specialisations, specialisation) {
             ...
         }
     }
🧹 Nitpick comments (7)
internal/systemd/unit.go (1)

126-133: Consider documenting boolean parsing semantics.

The ParseBool function implements systemd's boolean parsing logic but doesn't document which values map to true vs false. This is important for maintainability since systemd has specific semantics.

Add a function comment:

 // Parse a systemd boolean value string into a Go boolean.
+// Returns true for "1", "yes", "true", "on" (case-sensitive).
+// All other values, including "0", "no", "false", "off", return false.
 func ParseBool(value string) bool {
cmd/activate/user.go (2)

15-39: Consider validating UID/GID parameters.

The execUserSwitchProcess function accepts uid and gid parameters but doesn't validate they are non-zero or represent valid system users. While the caller may be trusted, defensive validation can prevent subtle bugs.

Consider adding basic validation:

 func execUserSwitchProcess(uid uint32, gid uint32, runtimePath string) error {
+  if uid == 0 || gid == 0 {
+    return fmt.Errorf("uid (%d) and gid (%d) must be non-zero for user switch", uid, gid)
+  }
+
   exe, err := filepath.EvalSymlinks("/proc/self/exe")
   if err != nil {
     return err
   }

64-68: Document why ReexecuteContext errors are ignored.

The comment explains that the systemd user session doesn't send a Reloaded signal and that dbus session bus errors are expected, but it would be helpful to clarify under what conditions errors are expected and whether any errors should be propagated.

Consider enhancing the comment:

 // The systemd user session seems to not send a Reloaded signal,
 // so we don't have anything to wait on here.
 //
 // Also, ignore errors, since the dbus session bus will probably
-// return an error here due to it running in the user's context.
+// return an error here due to it running in the user's context
+// and not having sufficient privileges for certain operations.
+// These errors are harmless and can be safely ignored.
 _ = systemd.ReexecuteContext(ctx)
module.nix (1)

144-154: Minor polish: wrapProgram arg quoting and comment accuracy.

  • Quote/escape SYSTEMD like others for consistency (paths are safe but consistency helps).
  • The comment mentions “nixos apply” but this module wires “nixos activate”.
-            --set SYSTEMD ${config.systemd.package}
+            --set SYSTEMD ${lib.escapeShellArg config.systemd.package}
-  # Preserve NIXOS_CONFIG and NIXOS_CLI_CONFIG in sudo invocations of
-  # `nixos apply`. This is required in order to keep ownership across
+  # Preserve NIXOS_CONFIG and NIXOS_CLI_CONFIG in sudo invocations of
+  # `nixos activate`. This is required in order to keep ownership across

Also applies to: 151-154

cmd/activate/run.go (1)

168-186: Be robust to newline/whitespace in init-interface-version files.

Trim file contents before comparing to avoid false mismatches.

-    if string(currentInitInterfaceVersion) != string(newInitInterfaceVersion) {
+    if strings.TrimSpace(string(currentInitInterfaceVersion)) != strings.TrimSpace(string(newInitInterfaceVersion)) {
         return ErrMismatchedInterfaceVersion
     }
cmd/activate/unit.go (2)

192-200: Keep original unit names while sorting case-insensitively.

Current Sorted() lowercases outputs. Sort by Lower but return original strings.

 func (l UnitList) Sorted() []string {
-    keys := make([]string, 0, len(l))
-    for unit := range l {
-        keys = append(keys, strings.ToLower(unit))
-    }
-    sort.Strings(keys)
-    return keys
+    keys := make([]string, 0, len(l))
+    for unit := range l {
+        keys = append(keys, unit)
+    }
+    sort.Slice(keys, func(i, j int) bool {
+        return strings.ToLower(keys[i]) < strings.ToLower(keys[j])
+    })
+    return keys
 }

450-459: Avoid panic on Variant type assertion.

Be defensive when extracting ActiveState.

-    state := prop.Value.Value().(string)
-    return state == "active" || state == "activating", nil
+    v, ok := prop.Value.Value().(string)
+    if !ok {
+        return false, fmt.Errorf("ActiveState is not a string")
+    }
+    return v == "active" || v == "activating", nil
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf280c6 and cb18de5.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (17)
  • cmd/activate/activate.go (1 hunks)
  • cmd/activate/fs.go (1 hunks)
  • cmd/activate/run.go (1 hunks)
  • cmd/activate/unit.go (1 hunks)
  • cmd/activate/user.go (1 hunks)
  • cmd/generation/delete/delete.go (2 hunks)
  • cmd/generation/delete/resolver.go (2 hunks)
  • doc/man/nixos-cli-activate.1.scd (1 hunks)
  • doc/man/nixos-cli-env.5.scd (1 hunks)
  • go.mod (4 hunks)
  • internal/system/local.go (2 hunks)
  • internal/systemd/time.go (1 hunks)
  • internal/systemd/time_test.go (1 hunks)
  • internal/systemd/unit.go (1 hunks)
  • internal/systemd/unit_test.go (1 hunks)
  • module.nix (2 hunks)
  • package.nix (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • package.nix
  • internal/systemd/unit_test.go
  • internal/systemd/time_test.go
  • internal/system/local.go
  • cmd/activate/fs.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-20T02:59:26.104Z
Learnt from: water-sucks
PR: nix-community/nixos-cli#94
File: cmd/activate/run.go:245-281
Timestamp: 2025-10-20T02:59:26.104Z
Learning: Go 1.25 introduced the sync.WaitGroup.Go(f func()) method, which starts f in a new goroutine and automatically handles Add(1) and defer Done(). This is valid Go 1.25+ syntax and should not be flagged as an error.

Applied to files:

  • cmd/activate/run.go
🧬 Code graph analysis (6)
cmd/generation/delete/delete.go (1)
internal/systemd/time.go (1)
  • DurationFromTimeSpan (11-89)
cmd/activate/run.go (9)
internal/system/runner.go (3)
  • CommandRunner (10-13)
  • NewCommand (24-33)
  • Command (15-22)
internal/logger/logger.go (1)
  • Logger (11-20)
internal/activation/activation.go (3)
  • SwitchToConfigurationAction (131-131)
  • SwitchToConfiguration (182-206)
  • SwitchToConfigurationOptions (176-180)
internal/generation/specialisations.go (1)
  • CollectSpecialisations (17-34)
internal/constants/constants.go (1)
  • CurrentSystem (7-7)
cmd/activate/unit.go (4)
  • UnitList (36-36)
  • START_LIST_FILE (21-21)
  • RELOAD_LIST_FILE (23-23)
  • RESTART_LIST_FILE (22-22)
internal/cmd/opts/opts.go (1)
  • ActivateOpts (13-16)
internal/system/local.go (1)
  • NewLocalSystem (17-21)
internal/systemd/unit.go (1)
  • ParseUnit (20-58)
cmd/activate/activate.go (4)
internal/cmd/opts/opts.go (1)
  • ActivateOpts (13-16)
cmd/activate/run.go (1)
  • NIXOS_STC_PARENT_EXE (42-42)
internal/activation/activation.go (1)
  • ParseSwitchToConfigurationAction (142-157)
internal/cmd/utils/utils.go (3)
  • CommandErrorHandler (25-31)
  • SetHelpFlagText (15-17)
  • AlignedOptions (48-68)
cmd/activate/user.go (2)
cmd/activate/run.go (1)
  • NIXOS_STC_PARENT_EXE (42-42)
internal/logger/logger.go (1)
  • Logger (11-20)
cmd/generation/delete/resolver.go (1)
internal/systemd/time.go (1)
  • DurationFromTimeSpan (11-89)
cmd/activate/unit.go (2)
internal/systemd/unit.go (5)
  • ParseUnit (20-58)
  • CompareUnits (159-262)
  • UnitComparisonNeedsRestart (139-139)
  • UnitComparisonNeedsReload (140-140)
  • UnitInfo (12-12)
cmd/activate/fs.go (1)
  • Filesystem (14-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build/Test
🔇 Additional comments (10)
internal/systemd/time.go (1)

1-1: LGTM! Package rename avoids stdlib conflict.

The package declaration was updated from package time to package systemdUtils to avoid naming conflicts with Go's standard library time package. This is a sensible refactoring decision.

cmd/generation/delete/resolver.go (1)

12-12: LGTM! Import path correctly updated.

The import path and function call have been correctly updated to reflect the package reorganization from internal/time to internal/systemd. The usage of systemdUtils.DurationFromTimeSpan is consistent with the new package structure.

Also applies to: 96-96

cmd/generation/delete/delete.go (1)

24-24: LGTM! Consistent import path update.

The import and usage have been correctly updated to use the new internal/systemd package path, maintaining consistency across the codebase.

Also applies to: 45-45

doc/man/nixos-cli-activate.1.scd (1)

1-65: LGTM! Comprehensive documentation for the new activate command.

The manual page provides clear documentation for the nixos activate command, covering all action types, options, and usage patterns. The description appropriately notes this is a low-level command and explains the relationship with the switch-to-configuration script.

doc/man/nixos-cli-env.5.scd (2)

83-92: LGTM! Clear documentation for activation environment variables.

The new activation-related environment variables (NIXOS_NO_CHECK, NIXOS_NO_SYNC, STC_DISPLAY_ALL_UNITS) are well-documented with clear descriptions of their behavior and impact.


93-113: LGTM! Development variables properly segregated.

The new "DEVELOPMENT MODE VARIABLES" section appropriately separates internal/development-only variables from user-facing environment variables, with clear warnings not to set them directly. This improves documentation clarity.

cmd/activate/user.go (1)

78-83: Past review issue resolved: Error handling is now correct.

The previous review comment flagged that lines 78-81 were returning nil on activation failures. The code now correctly returns an error when the service fails to restart, properly propagating failures to the caller.

go.mod (2)

3-3: Dependency versions verified as secure.

Verification confirms:

  • carapace v1.8.6: No known vulnerabilities
  • google/shlex: No known vulnerabilities
  • golang.org/x/sys v0.34.0: Not vulnerable—the MODERATE CVE-2022-29526 privilege reporting bug was fixed before v0.34.0 release

All dependency versions are current and free from known security advisories. No action required.


9-9: Verify the purpose and version strategy of the go-systemd fork.

Fork verification shows: The fork (water-sucks/go-systemd) is maintained as of Oct 15, 2025, and no security vulnerabilities were found in the go-systemd package. However, several concerns warrant developer verification:

  • Minimal adoption: The fork has 0 stars and 0 forks, suggesting it's an internal/organizational fork with no community visibility.
  • No documentation: The fork repository lacks a README or accessible changelog explaining why it's needed.
  • Version mismatch: The replace directive pins v22.6.0 (current upstream) to a v22.0.0 pseudo-version (Oct 14, 2025), suggesting specific changes or patches. Verify these changes are intentional and compatible.
  • Upstream more active: The upstream coreos/go-systemd was updated Oct 20 (5 days after the fork), indicating it remains actively maintained. Confirm the fork does not fall behind critical upstream fixes.

Ensure the fork's purpose, patches, and compatibility with the current codebase are documented and justified.

cmd/activate/unit.go (1)

83-92: LGTM on classification fixes.

  • Active-state check now uses State correctly.
  • .path suffix handled.
  • Socket-activation guard prevents touching services directly.

Also applies to: 223-229, 357-375

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
module.nix (1)

102-112: Scope down sudo env_keep to nixos-cli only; avoid global leakage.

Keep env across sudo only for the nixos-cli/switch command, not globally. Also consider dropping DEBUG_MODE from env_keep.

Example (command-scoped Defaults):

-      security.sudo.extraConfig = ''
-        Defaults env_keep += "NIXOS_CONFIG"
-        Defaults env_keep += "NIXOS_GENERATION_TAG"
-        Defaults env_keep += "NIXOS_CLI_CONFIG"
-        Defaults env_keep += "NIXOS_CLI_DISABLE_STEPS"
-        Defaults env_keep += "NIXOS_CLI_DEBUG_MODE"
-        Defaults env_keep += "NIXOS_CLI_SUPPRESS_NO_SETTINGS_WARNING"
-      '';
+      security.sudo.extraConfig = ''
+        # Restrict preserved env to nixos-cli binary
+        Defaults!${lib.getExe cfg.package} env_keep += "NIXOS_CONFIG NIXOS_GENERATION_TAG NIXOS_CLI_CONFIG NIXOS_CLI_DISABLE_STEPS NIXOS_CLI_SUPPRESS_NO_SETTINGS_WARNING"
+      '';
cmd/activate/run.go (2)

332-336: Grammar: “must be run as root.”

-		err := fmt.Errorf("this command must be ran as root")
+		err := fmt.Errorf("this command must be run as root")

117-121: Fix specialisation error: wrong arg and unclear message.

Use the requested specialisation in the error; no need to log first if caller logs.

-		if !slices.Contains(specialisations, specialisation) {
-			err = fmt.Errorf("specialisation '%v' does not exist", specialisations)
-			log.Error(err)
-			return err
-		}
+		if !slices.Contains(specialisations, specialisation) {
+			return fmt.Errorf("specialisation defined in $NIXOS_SPECIALISATION %q does not exist", specialisation)
+		}
🧹 Nitpick comments (9)
internal/system/local.go (3)

50-50: Consider a more precise regex pattern.

The current pattern ^\"?nixos\"?$ will match edge cases like "nixos (quote only at start) or nixos" (quote only at end). While unlikely in practice for the os-release ID field, a more precise pattern would be:

var nixosDistroIDRegex = regexp.MustCompile(`^(?:"nixos"|nixos)$`)

This explicitly matches either quoted or unquoted variants without allowing mismatched quotes.


82-82: Simplify the defer close pattern.

The anonymous function wrapper is unnecessarily verbose. The idiomatic Go pattern for deferring a close is simply:

defer osRelease.Close()

For read-only files, the close error is typically not critical, and the standard pattern doesn't require explicit error suppression.

Apply this diff to simplify:

-	defer func() { _ = osRelease.Close() }()
+	defer osRelease.Close()

87-95: Check scanner error after loop.

After scanning completes, the scanner's Err() method should be checked to detect any errors that occurred during scanning (e.g., I/O errors).

Apply this diff to add error checking:

 	for s.Scan() {
 		key, value, found := strings.Cut(s.Text(), "=")
 		if !found {
 			continue
 		}
 		values[key] = value
 	}

+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+
 	return values, nil
 }
doc/man/nixos-cli-env.5.scd (3)

83-92: Align env var semantics with code (presence vs “1”).

Docs say “set to 1,” but run.go currently treats any non-empty value as “set.” Either:

  • Change code to check for value == "1" (recommended; see my run.go diff), or
  • Update these lines to say “if set (non-empty)”.

Please pick one for consistency.


90-92: Clarify accepted values.

State explicitly “Set to 1 to show all units; unset or other values hide non-essential units.” Matches current usage in code.


103-106: Grammar nit.

“Only useful for during development.” → “Only useful during development.”

-	Show log messages for when developing TUIs. Only useful for during
-	development.
+	Show log messages when developing TUIs. Only useful during development.
cmd/activate/run.go (1)

749-753: Document checkpoint/resume intent for list files.

Consider adding a brief comment where you record/unrecord units and where you RemoveAll to explain the resilience pattern (persist before action; clean on success). Reduces future confusion.

Also applies to: 757-758, 772-785

cmd/activate/unit.go (2)

192-200: Preserve original unit names while sorting case-insensitively.

Current implementation lowercases returned names. Keep original strings for readability.

-func (l UnitList) Sorted() []string {
-	keys := make([]string, 0, len(l))
-	for unit := range l {
-		keys = append(keys, strings.ToLower(unit))
-	}
-	sort.Strings(keys)
-	return keys
-}
+func (l UnitList) Sorted() []string {
+	keys := make([]string, 0, len(l))
+	for unit := range l {
+		keys = append(keys, unit)
+	}
+	sort.Slice(keys, func(i, j int) bool {
+		return strings.ToLower(keys[i]) < strings.ToLower(keys[j])
+	})
+	return keys
+}

533-542: Handle scanner error (optional).

Capture scanner.Err() to avoid silently ignoring read errors.

 s := bufio.NewScanner(file)
 for s.Scan() {
   // ...
 }
-
-return units
+if err := s.Err(); err != nil {
+  // best-effort: ignore content on error
+  return units
+}
+return units
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb18de5 and dde565a.

📒 Files selected for processing (10)
  • cmd/activate/activate.go (1 hunks)
  • cmd/activate/run.go (1 hunks)
  • cmd/activate/unit.go (1 hunks)
  • cmd/activate/user.go (1 hunks)
  • doc/man/nixos-cli-activate.1.scd (1 hunks)
  • doc/man/nixos-cli-env.5.scd (1 hunks)
  • go.mod (4 hunks)
  • internal/system/local.go (2 hunks)
  • module.nix (2 hunks)
  • package.nix (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • doc/man/nixos-cli-activate.1.scd
🚧 Files skipped from review as they are similar to previous changes (3)
  • cmd/activate/activate.go
  • cmd/activate/user.go
  • package.nix
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-20T02:59:26.104Z
Learnt from: water-sucks
PR: nix-community/nixos-cli#94
File: cmd/activate/run.go:245-281
Timestamp: 2025-10-20T02:59:26.104Z
Learning: Go 1.25 introduced the sync.WaitGroup.Go(f func()) method, which starts f in a new goroutine and automatically handles Add(1) and defer Done(). This is valid Go 1.25+ syntax and should not be flagged as an error.

Applied to files:

  • cmd/activate/run.go
📚 Learning: 2025-10-20T20:32:37.692Z
Learnt from: water-sucks
PR: nix-community/nixos-cli#94
File: cmd/activate/run.go:752-761
Timestamp: 2025-10-20T20:32:37.692Z
Learning: In cmd/activate/run.go, the global list files (START_LIST_FILE, RELOAD_LIST_FILE, RESTART_LIST_FILE) implement a checkpoint/recovery pattern for activation resilience: they are read at start (unit.go), written during processing before actions are taken (for persistence across interruptions), and removed after successful completion (for cleanup). If the activation process is interrupted, the next run loads these files to complete unfinished unit actions.

Applied to files:

  • cmd/activate/run.go
  • cmd/activate/unit.go
🧬 Code graph analysis (2)
cmd/activate/run.go (8)
internal/system/runner.go (3)
  • CommandRunner (10-13)
  • NewCommand (24-33)
  • Command (15-22)
internal/activation/activation.go (3)
  • SwitchToConfigurationAction (131-131)
  • SwitchToConfiguration (182-206)
  • SwitchToConfigurationOptions (176-180)
internal/generation/specialisations.go (1)
  • CollectSpecialisations (17-34)
internal/constants/constants.go (1)
  • CurrentSystem (7-7)
cmd/activate/unit.go (4)
  • UnitList (36-36)
  • START_LIST_FILE (21-21)
  • RELOAD_LIST_FILE (23-23)
  • RESTART_LIST_FILE (22-22)
internal/cmd/opts/opts.go (1)
  • ActivateOpts (13-16)
internal/system/local.go (1)
  • NewLocalSystem (17-21)
internal/systemd/unit.go (1)
  • ParseUnit (20-58)
cmd/activate/unit.go (2)
internal/systemd/unit.go (5)
  • ParseUnit (20-58)
  • CompareUnits (159-262)
  • UnitComparisonNeedsRestart (139-139)
  • UnitComparisonNeedsReload (140-140)
  • UnitInfo (12-12)
cmd/activate/fs.go (1)
  • Filesystem (14-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build/Test
🔇 Additional comments (7)
go.mod (4)

3-3: Verify Go version 1.25 is intentional.

Go 1.25 has not been released as of the project's knowledge cutoff. Confirm whether this is a forward-declaration for planned support or should be pinned to 1.24.


13-13: Verify shlex version strategy.

The commit-based pseudo-version v0.0.0-20191202100458-e7afc7fbc510 dates to 2019 and does not reference a release tag. Confirm this is intentional (no newer releases available) or consider pinning to an available release tag if one exists.

Based on learnings, shlex is a stable, minimal library; the pseudo-version is safe but may indicate a maintenance gap in the upstream.


94-94: Clarify the go-systemd fork strategy and pseudo-version date.

The replace directive substitutes the official coreos/go-systemd with a fork (water-sucks/go-systemd) pinned to a commit-based pseudo-version. Two concerns:

  1. Fork maintenance risk: Using a fork introduces a maintenance burden and divergence from upstream. Document why this fork is required (custom patches, compatibility fixes, etc.) and ensure it is actively maintained.

  2. Pseudo-version date inconsistency: The pseudo-version date 20251014 (2025-10-14) appears to be after the PR creation date (2025-07-27). Verify this is not a data entry error or confirm the commit date is correct.


52-52: Confirm glamour is no longer directly imported.

The classification change from direct to indirect (line 52) suggests that charmbracelet/glamour is no longer explicitly imported in the application code but is pulled in transitively. Verify this is intentional and that any CLI features relying on glamour (markdown rendering) remain functional.

internal/system/local.go (2)

4-4: LGTM!

The new imports are appropriate for the os-release parsing functionality.

Also applies to: 7-8


52-69: LGTM!

The IsNixOS detection logic is well-structured with a fast path for the /etc/NIXOS marker file and a fallback to parsing /etc/os-release. Error handling is appropriate.

cmd/activate/run.go (1)

246-252: No action needed—toolchain already at Go 1.25.

The go.mod file already declares go 1.25, which satisfies the minimum version requirement for sync.WaitGroup.Go(). The code change is compatible.

@water-sucks water-sucks merged commit ce22863 into nix-community:main Oct 20, 2025
1 of 2 checks passed
@water-sucks water-sucks deleted the activation branch October 20, 2025 20:52
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
cmd/activate/run.go (3)

387-395: Enforce explicit "1" value for NIXOS_NO_CHECK.

The current logic skips checks when NIXOS_NO_CHECK is empty, but the environment variable semantics should require the literal value "1" to disable checks (any other value, including empty, should run checks).

Apply this diff:

-	if skipCheck := os.Getenv("NIXOS_NO_CHECK"); skipCheck == "" {
+	if os.Getenv("NIXOS_NO_CHECK") != "1" {
 		log.Info("running pre-switch checks")
 
 		err = runPreSwitchCheck(s, vars.PreSwitchCheckCmd, vars.Toplevel, opts.Action)

410-426: Enforce explicit "1" value for NIXOS_NO_SYNC.

The current logic syncs when NIXOS_NO_SYNC is empty, but the environment variable semantics should require the literal value "1" to skip syncing (any other value, including empty, should perform the sync).

Apply this diff:

-	if skipSync := os.Getenv("NIXOS_NO_SYNC"); skipSync == "" {
+	if os.Getenv("NIXOS_NO_SYNC") != "1" {
 		log.Info("syncing /nix/store to disk")
 
 		dir, err := os.Open("/nix/store")

332-336: Fix grammar in error message.

Apply this diff:

 	if os.Geteuid() != 0 {
-		err := fmt.Errorf("this command must be ran as root")
+		err := fmt.Errorf("this command must be run as root")
 		log.Errorf("%s", err)
 		return err
 	}
module.nix (1)

102-112: Security concern: Global sudo env_keep scope remains unaddressed.

The previous review raised a security concern about preserving NIXOS_* variables globally across all sudo invocations. This issue has not been addressed in the current changes. These variables can leak control to arbitrary sudo sessions beyond nixos activate or nixos apply.

As suggested in the previous review, restrict the env_keep settings to a specific command alias:

  1. Define a Cmnd_Alias for the nixos-cli commands
  2. Apply env_keep only to that alias via a Runas_Spec

Example approach:

security.sudo.extraConfig = ''
  # Define command alias for nixos-cli activation
  Cmnd_Alias NIXOS_CLI = ${lib.getExe cfg.package}
  
  # Preserve NIXOS_* variables only for nixos-cli commands
  Defaults!NIXOS_CLI env_keep += "NIXOS_CONFIG NIXOS_GENERATION_TAG NIXOS_CLI_CONFIG NIXOS_CLI_DISABLE_STEPS NIXOS_CLI_DEBUG_MODE NIXOS_CLI_SUPPRESS_NO_SETTINGS_WARNING"
'';

This minimizes the blast radius by ensuring these variables are preserved only for the specific nixos-cli executable.

🧹 Nitpick comments (1)
go.mod (1)

13-13: Consider migrating to carapace-shlex to avoid relying on an archived library.

The google/shlex repository is archived and hasn't received updates since 2019, meaning it won't receive security patches or bug fixes. The codebase already has carapace-shlex v1.0.1 as an indirect dependency, which is an actively maintained fork with the same Split() API. Since google/shlex is only used twice in cmd/activate/run.go (lines 137 and 155), migration is straightforward.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dde565a and 0401cde.

📒 Files selected for processing (8)
  • cmd/activate/activate.go (1 hunks)
  • cmd/activate/run.go (1 hunks)
  • doc/man/nixos-cli-activate.1.scd (1 hunks)
  • doc/man/nixos-cli-env.5.scd (1 hunks)
  • go.mod (4 hunks)
  • internal/system/local.go (2 hunks)
  • module.nix (2 hunks)
  • package.nix (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • doc/man/nixos-cli-env.5.scd
  • package.nix
  • internal/system/local.go
  • doc/man/nixos-cli-activate.1.scd
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-20T02:59:26.104Z
Learnt from: water-sucks
PR: nix-community/nixos-cli#94
File: cmd/activate/run.go:245-281
Timestamp: 2025-10-20T02:59:26.104Z
Learning: Go 1.25 introduced the sync.WaitGroup.Go(f func()) method, which starts f in a new goroutine and automatically handles Add(1) and defer Done(). This is valid Go 1.25+ syntax and should not be flagged as an error.

Applied to files:

  • cmd/activate/run.go
📚 Learning: 2025-10-20T20:32:37.692Z
Learnt from: water-sucks
PR: nix-community/nixos-cli#94
File: cmd/activate/run.go:752-761
Timestamp: 2025-10-20T20:32:37.692Z
Learning: In cmd/activate/run.go, the global list files (START_LIST_FILE, RELOAD_LIST_FILE, RESTART_LIST_FILE) implement a checkpoint/recovery pattern for activation resilience: they are read at start (unit.go), written during processing before actions are taken (for persistence across interruptions), and removed after successful completion (for cleanup). If the activation process is interrupted, the next run loads these files to complete unfinished unit actions.

Applied to files:

  • cmd/activate/run.go
🧬 Code graph analysis (2)
cmd/activate/activate.go (4)
internal/cmd/opts/opts.go (1)
  • ActivateOpts (13-16)
cmd/activate/run.go (1)
  • NIXOS_STC_PARENT_EXE (42-42)
internal/activation/activation.go (1)
  • ParseSwitchToConfigurationAction (142-157)
internal/cmd/utils/utils.go (3)
  • CommandErrorHandler (25-31)
  • SetHelpFlagText (15-17)
  • AlignedOptions (48-68)
cmd/activate/run.go (8)
internal/system/runner.go (3)
  • CommandRunner (10-13)
  • NewCommand (24-33)
  • Command (15-22)
internal/activation/activation.go (7)
  • SwitchToConfigurationAction (131-131)
  • SwitchToConfiguration (182-206)
  • SwitchToConfigurationOptions (176-180)
  • SwitchToConfigurationActionDryActivate (139-139)
  • SwitchToConfigurationActionChecksOnly (135-135)
  • SwitchToConfigurationActionBoot (137-137)
  • SwitchToConfigurationActionSwitch (136-136)
internal/generation/specialisations.go (1)
  • CollectSpecialisations (17-34)
internal/constants/constants.go (1)
  • CurrentSystem (7-7)
cmd/activate/unit.go (4)
  • UnitList (36-36)
  • START_LIST_FILE (21-21)
  • RELOAD_LIST_FILE (23-23)
  • RESTART_LIST_FILE (22-22)
internal/cmd/opts/opts.go (1)
  • ActivateOpts (13-16)
internal/system/local.go (1)
  • NewLocalSystem (17-21)
internal/systemd/unit.go (1)
  • ParseUnit (20-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build/Test
🔇 Additional comments (7)
cmd/activate/activate.go (1)

13-67: LGTM! Well-structured command with context-aware validation.

The command implementation is solid:

  • Context-sensitive Args validation correctly allows omitting ACTION when NIXOS_STC_PARENT_EXE is set (line 29)
  • Shell completion provides user-friendly descriptions
  • Conditional flag exposure prevents --specialisation in activation context
  • Help template enhancement via AlignedOptions improves UX
go.mod (2)

3-3: LGTM: Go 1.25 upgrade aligns with dependency requirements.

The upgrade to Go 1.25 is appropriate, especially given that golang.org/x/sys v0.34.0 may require a recent toolchain.


94-94: Clarify necessity and stability of the go-systemd fork.

The replace directive uses water-sucks/go-systemd commit fe9dbfc225a6 (from feature branch add-reexecute-method) instead of a stable release. Investigation shows:

  • Fork adds two new dbus methods: ReexecuteContext() and ResetFailedContext()—not present in official v22.6.0
  • A related upstream PR #430 ("Bugfix/dbus methods context", opened Oct 2023) remains unmerged and unreviewed for 2 years, suggesting low upstream priority or design concerns
  • Fork created Oct 14, 2025, without tags or releases

Concerns:

  • Using a commit hash from a feature branch lacks stability guarantees
  • Fork maintenance becomes project responsibility with unclear timeline
  • No evidence of upstream contribution plans or temporary status

Verification needed:

  1. Are ReexecuteContext() and ResetFailedContext() required for this project's core functionality, or are they optional enhancements?
  2. Is this fork temporary (pending upstream merge) or permanent? If permanent, tag a stable release instead of using a commit hash.
  3. Have you considered contributing these methods to coreos/go-systemd, or is there a reason upstream would not accept them?
module.nix (4)

48-65: LGTM: Well-documented experimental option.

The useActivationInterface option is properly structured with clear documentation, appropriate defaults, and an explicit experimental status warning. The conservative default (false) is suitable for a new activation interface.


81-100: LGTM: Clean config restructuring and metadata injection.

The restructured configuration using mkMerge is well-organized. Moving the config to /etc/nixos-cli/config.toml and injecting nixos-version.json via system builder commands are appropriate changes that support the activation interface.


114-128: LGTM: Option cache generation is well-structured.

The conditional option cache block properly filters visible, non-internal options and places the cache in a consistent location (/etc/nixos-cli/options-cache.json). The conditional on cfg.prebuildOptionCache allows users to control eval-time tradeoffs.


129-156: Code changes are well-isolated; verify nixpkgs integration.

The system.switch.enable = lib.mkForce false change is contained to this module with no breaking references found in the codebase. The existing code comment already documents the intended behavior: disabling switch-to-configuration-ng while preserving system switchability.

Verify that this configuration integrates correctly with your nixpkgs version and that system rollback and activation operations work as expected in your environment.

@Malix-Labs
Copy link
Copy Markdown

Malix-Labs commented Oct 20, 2025

#94 (comment) from @water-sucks :

[switch-to-configuration-ng] is not suitable for the purposes of a self-contained binary

Can you explain wny?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

replace switch-to-configuration in nixpkgs w/ home-grown implementation

2 participants