confctl 2.2.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.git-hooks/pre_commit/nixfmt.rb +13 -0
- data/.github/workflows/rspec.yml +64 -0
- data/.github/workflows/rubocop.yml +27 -0
- data/.github/workflows/tests.yml +139 -0
- data/.gitignore +12 -7
- data/.overcommit.yml +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +8 -1
- data/AGENTS.md +42 -0
- data/CHANGELOG.md +57 -0
- data/Gemfile +3 -2
- data/Gemfile.lock +198 -0
- data/README.md +166 -58
- data/Rakefile +5 -0
- data/confctl.gemspec +20 -2
- data/docs/carrier.md +3 -3
- data/docs/flake-inputs.md +159 -0
- data/docs/swpins-to-flakes.md +325 -0
- data/example/cluster/module-list.nix +2 -1
- data/example/cluster/nixos-machine/config.nix +12 -2
- data/example/cluster/nixos-machine/hardware.nix +6 -1
- data/example/cluster/vpsadminos-container/config.nix +6 -1
- data/example/cluster/vpsadminos-container/module.nix +4 -1
- data/example/cluster/vpsadminos-machine/config.nix +6 -1
- data/example/cluster/vpsadminos-machine/hardware.nix +6 -1
- data/example/cluster/vpsadminos-machine/module.nix +5 -2
- data/example/cluster/vpsfreecz-vps/config.nix +6 -1
- data/example/cluster/vpsfreecz-vps/module.nix +4 -1
- data/example/configs/swpins.nix +8 -3
- data/example/environments/base.nix +6 -1
- data/example/swpins/core.json +35 -0
- data/example-flake/.gitignore +2 -0
- data/example-flake/README.md +38 -0
- data/example-flake/cluster/cluster.nix +5 -0
- data/example-flake/cluster/module-list.nix +4 -0
- data/example-flake/cluster/nested/nixos-machine/config.nix +25 -0
- data/example-flake/cluster/nested/nixos-machine/hardware.nix +9 -0
- data/example-flake/cluster/nested/nixos-machine/module.nix +8 -0
- data/example-flake/cluster/nixos-machine/config.nix +25 -0
- data/example-flake/cluster/nixos-machine/hardware.nix +9 -0
- data/example-flake/cluster/nixos-machine/module.nix +8 -0
- data/example-flake/cluster/vpsadminos-container/config.nix +28 -0
- data/example-flake/cluster/vpsadminos-container/module.nix +8 -0
- data/example-flake/cluster/vpsadminos-machine/config.nix +27 -0
- data/example-flake/cluster/vpsadminos-machine/hardware.nix +9 -0
- data/example-flake/cluster/vpsadminos-machine/module.nix +8 -0
- data/example-flake/cluster/vpsfreecz-vps/config.nix +31 -0
- data/example-flake/cluster/vpsfreecz-vps/module.nix +8 -0
- data/example-flake/configs/confctl.nix +10 -0
- data/example-flake/data/default.nix +5 -0
- data/example-flake/data/ssh-keys.nix +7 -0
- data/example-flake/environments/base.nix +18 -0
- data/example-flake/flake.lock +75 -0
- data/example-flake/flake.nix +36 -0
- data/example-flake/modules/module-list.nix +13 -0
- data/example-flake/shell.nix +11 -0
- data/flake.lock +159 -0
- data/flake.nix +189 -0
- data/gemset.nix +1022 -0
- data/lib/confctl/cli/app.rb +145 -0
- data/lib/confctl/cli/attr_filters.rb +1 -1
- data/lib/confctl/cli/cluster.rb +681 -108
- data/lib/confctl/cli/command.rb +24 -2
- data/lib/confctl/cli/configuration.rb +171 -106
- data/lib/confctl/cli/generation.rb +65 -1
- data/lib/confctl/cli/inputs/channels.rb +190 -0
- data/lib/confctl/cli/inputs/machines.rb +83 -0
- data/lib/confctl/cli/inputs/root.rb +101 -0
- data/lib/confctl/cli/inputs.rb +5 -0
- data/lib/confctl/cli/log_view.rb +21 -10
- data/lib/confctl/cli/migrate/swpins_to_flakes.rb +866 -0
- data/lib/confctl/cli/migrate.rb +5 -0
- data/lib/confctl/cli/output_formatter.rb +5 -7
- data/lib/confctl/cli/swpins/base.rb +9 -0
- data/lib/confctl/cli/swpins/channel.rb +2 -5
- data/lib/confctl/cli/swpins/cluster.rb +2 -5
- data/lib/confctl/cli/swpins/core.rb +2 -5
- data/lib/confctl/config_type.rb +7 -0
- data/lib/confctl/flake_lock.rb +78 -0
- data/lib/confctl/flake_lock_diff.rb +36 -0
- data/lib/confctl/generation/build.rb +131 -24
- data/lib/confctl/generation/build_list.rb +4 -3
- data/lib/confctl/generation/unified.rb +14 -1
- data/lib/confctl/git_repo_mirror.rb +2 -2
- data/lib/confctl/health_checks/run_command.rb +3 -2
- data/lib/confctl/health_checks/systemd/properties.rb +1 -1
- data/lib/confctl/health_checks/systemd/property_list.rb +2 -2
- data/lib/confctl/inputs/commit_message.rb +125 -0
- data/lib/confctl/inputs/git_commit.rb +17 -0
- data/lib/confctl/inputs/nix_output_guard.rb +37 -0
- data/lib/confctl/inputs/setter.rb +179 -0
- data/lib/confctl/inputs/updater.rb +76 -0
- data/lib/confctl/inputs.rb +5 -0
- data/lib/confctl/inputs_info.rb +50 -0
- data/lib/confctl/line_buffer.rb +1 -1
- data/lib/confctl/machine.rb +18 -3
- data/lib/confctl/machine_control.rb +8 -2
- data/lib/confctl/machine_list.rb +2 -2
- data/lib/confctl/machine_status.rb +63 -20
- data/lib/confctl/nix/args.rb +48 -0
- data/lib/confctl/nix.rb +30 -437
- data/lib/confctl/nix_build_flake.rb +95 -0
- data/lib/confctl/nix_copy.rb +14 -2
- data/lib/confctl/nix_flake.rb +449 -0
- data/lib/confctl/nix_format.rb +3 -3
- data/lib/confctl/nix_legacy.rb +467 -0
- data/lib/confctl/swpins/change_set.rb +29 -3
- data/lib/confctl/swpins/specs/base.rb +2 -2
- data/lib/confctl/ui.rb +19 -0
- data/lib/confctl/version.rb +1 -1
- data/man/index.html +11 -0
- data/man/man8/confctl-options.nix.8.html +113 -0
- data/man/man8/confctl.8 +152 -21
- data/man/man8/confctl.8.html +355 -0
- data/man/man8/confctl.8.md +148 -17
- data/man/style.css +301 -0
- data/nix/evaluator.nix +94 -69
- data/nix/flake/mk-confctl-devshell.nix +85 -0
- data/nix/flake/mk-confctl-outputs.nix +522 -0
- data/nix/flake/mk-config-devshell.nix +168 -0
- data/nix/lib/default.nix +118 -65
- data/nix/lib/machine/default.nix +65 -45
- data/nix/lib/machine/info.nix +16 -5
- data/nix/lib/swpins/eval.nix +42 -29
- data/nix/lib/swpins/options.nix +6 -2
- data/nix/machines.nix +23 -15
- data/nix/modules/cluster/default.nix +104 -41
- data/nix/modules/confctl/carrier/base.nix +11 -4
- data/nix/modules/confctl/carrier/carrier-env.rb +2 -2
- data/nix/modules/confctl/carrier/netboot/build-netboot-server.rb +50 -16
- data/nix/modules/confctl/carrier/netboot/nixos.nix +56 -24
- data/nix/modules/confctl/configuration-info.nix +17 -0
- data/nix/modules/confctl/generations.nix +2 -2
- data/nix/modules/confctl/host.nix +13 -0
- data/nix/modules/confctl/inputs-info.nix +21 -0
- data/nix/modules/confctl/kexec-netboot/default.nix +13 -6
- data/nix/modules/confctl/kexec-netboot/kexec-netboot.8.adoc +3 -0
- data/nix/modules/confctl/kexec-netboot/kexec-netboot.rb +25 -16
- data/nix/modules/confctl/nix.nix +31 -1
- data/nix/modules/confctl/swpins.nix +13 -6
- data/nix/modules/module-list.nix +4 -2
- data/nix/modules/system-list.nix +4 -1
- data/nix/package.nix +37 -0
- data/shell.nix +39 -8
- data/skills/confctl-configuration-update/SKILL.md +228 -0
- data/skills/confctl-configuration-update/agents/openai.yaml +4 -0
- data/skills/confctl-release/SKILL.md +102 -0
- data/skills/confctl-release/agents/openai.yaml +4 -0
- data/spec/confctl/cli/cluster_health_check_spec.rb +43 -0
- data/spec/confctl/cli/cluster_skip_current_deploy_spec.rb +157 -0
- data/spec/confctl/cli/cluster_status_flake_spec.rb +83 -0
- data/spec/confctl/cli/inputs_set_output_spec.rb +70 -0
- data/spec/confctl/configuration_spec.rb +45 -0
- data/spec/confctl/inputs_spec.rb +104 -0
- data/spec/confctl/machine_control_spec.rb +112 -0
- data/spec/confctl/machine_list_spec.rb +47 -0
- data/spec/confctl/nix_flake_spec.rb +41 -0
- data/spec/confctl/swpins_spec.rb +106 -0
- data/spec/generation/build_modes_spec.rb +132 -0
- data/spec/inputs/commit_message_spec.rb +205 -0
- data/spec/inputs/nix_cached_fallback_spec.rb +50 -0
- data/spec/inputs/nix_output_guard_spec.rb +45 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/cli_helper.rb +57 -0
- data/test-runner.sh +7 -0
- data/tests/all-tests.nix +30 -0
- data/tests/make-test.nix +15 -0
- data/tests/runner/extensions/confctl_helpers.rb +238 -0
- data/tests/runner/extensions/hostfwd_ports.rb +41 -0
- data/tests/suite/auto_rollback.nix +344 -0
- data/tests/suite/carrier/deploy.nix +751 -0
- data/tests/suite/carrier/netboot.nix +849 -0
- data/tests/suite/deploy/base.nix +806 -0
- data/tests/suite/deploy/flakes.nix +1 -0
- data/tests/suite/deploy/swpins.nix +1 -0
- metadata +104 -4
- data/nix/modules/confctl/overlays.nix +0 -15
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
require 'confctl/cli/command'
|
|
2
|
+
require 'confctl/conf_dir'
|
|
3
|
+
require 'confctl/inputs/setter'
|
|
4
|
+
require 'confctl/nix_legacy'
|
|
5
|
+
require 'confctl/system_command'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'json'
|
|
8
|
+
|
|
9
|
+
module ConfCtl::Cli
|
|
10
|
+
class Migrate::SwpinsToFlakes < Command
|
|
11
|
+
INPUTS_BEGIN = '# confctl migrate swpins-to-flakes: BEGIN inputs'.freeze
|
|
12
|
+
INPUTS_END = '# confctl migrate swpins-to-flakes: END inputs'.freeze
|
|
13
|
+
CHANNELS_BEGIN = '# confctl migrate swpins-to-flakes: BEGIN channels'.freeze
|
|
14
|
+
CHANNELS_END = '# confctl migrate swpins-to-flakes: END channels'.freeze
|
|
15
|
+
|
|
16
|
+
def all
|
|
17
|
+
return unless flake
|
|
18
|
+
return unless machines
|
|
19
|
+
|
|
20
|
+
imports
|
|
21
|
+
clean
|
|
22
|
+
|
|
23
|
+
puts "\nNext steps:"
|
|
24
|
+
puts ' nix develop'
|
|
25
|
+
puts ' confctl ls'
|
|
26
|
+
puts ' confctl build <one-machine>'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def flake
|
|
30
|
+
step('Flake')
|
|
31
|
+
|
|
32
|
+
flake_path = File.join(conf_dir, 'flake.nix')
|
|
33
|
+
swpins_config = File.join(conf_dir, 'configs', 'swpins.nix')
|
|
34
|
+
core_path = File.join(conf_dir, 'swpins', 'core.json')
|
|
35
|
+
|
|
36
|
+
raise ConfCtl::Error, 'flake.nix already exists' if File.exist?(flake_path)
|
|
37
|
+
raise ConfCtl::Error, 'configs/swpins.nix missing' unless File.exist?(swpins_config)
|
|
38
|
+
raise ConfCtl::Error, 'swpins/core.json missing; update swpins state first' unless File.exist?(core_path)
|
|
39
|
+
|
|
40
|
+
legacy_channels = load_legacy_channels
|
|
41
|
+
channel_names = legacy_channels.keys.sort
|
|
42
|
+
|
|
43
|
+
channel_state = {}
|
|
44
|
+
channel_names.each do |channel|
|
|
45
|
+
path = File.join(conf_dir, 'swpins', 'channels', "#{channel}.json")
|
|
46
|
+
unless File.exist?(path)
|
|
47
|
+
raise ConfCtl::Error,
|
|
48
|
+
"swpins/channels/#{channel}.json missing; update swpins state first"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
channel_state[channel] = load_json(path, "swpins/channels/#{channel}.json")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
core_json = load_json(core_path, 'swpins/core.json')
|
|
55
|
+
core_entry = core_json['nixpkgs']
|
|
56
|
+
raise ConfCtl::Error, 'swpins/core.json missing nixpkgs entry; update swpins state first' unless core_entry
|
|
57
|
+
|
|
58
|
+
core_spec = build_pin_spec(core_entry, 'core nixpkgs')
|
|
59
|
+
core_url = flake_url_for(core_spec)
|
|
60
|
+
|
|
61
|
+
plan = build_channel_plan(legacy_channels, channel_state, core_spec)
|
|
62
|
+
input_defs = plan[:input_defs]
|
|
63
|
+
channels = plan[:channels]
|
|
64
|
+
input_revs = plan[:input_revs]
|
|
65
|
+
|
|
66
|
+
flake_content = render_flake(core_url, input_defs, channels)
|
|
67
|
+
@generated_flake_content = flake_content
|
|
68
|
+
|
|
69
|
+
ops = []
|
|
70
|
+
ops << [:create, 'flake.nix']
|
|
71
|
+
ops << [:run, 'nix flake lock']
|
|
72
|
+
ops << [:set, "root nixpkgs rev #{core_spec[:rev]}"] if core_spec[:rev]
|
|
73
|
+
|
|
74
|
+
channels.keys.sort.each do |channel|
|
|
75
|
+
channels[channel].keys.sort.each do |role|
|
|
76
|
+
input_name = channels[channel][role]
|
|
77
|
+
rev = input_revs[input_name]
|
|
78
|
+
next unless rev
|
|
79
|
+
|
|
80
|
+
ops << [:set, "channel #{channel} role #{role} rev #{rev}"]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
return false unless confirm_step(ops, label: 'flake migration', required: true)
|
|
85
|
+
return true if dry_run?
|
|
86
|
+
|
|
87
|
+
write_atomic(flake_path, flake_content)
|
|
88
|
+
run_nix_flake_lock!
|
|
89
|
+
apply_input_revisions(input_revs)
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def machines
|
|
94
|
+
step('Machines')
|
|
95
|
+
|
|
96
|
+
flake_path = File.join(conf_dir, 'flake.nix')
|
|
97
|
+
|
|
98
|
+
cluster_dir = File.join(conf_dir, 'cluster')
|
|
99
|
+
module_paths = Dir.glob(File.join(cluster_dir, '**', 'module.nix'))
|
|
100
|
+
if module_paths.empty?
|
|
101
|
+
puts 'No cluster/**/module.nix files found.'
|
|
102
|
+
return true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
flake_content =
|
|
106
|
+
if File.exist?(flake_path)
|
|
107
|
+
File.read(flake_path)
|
|
108
|
+
elsif dry_run? && @generated_flake_content
|
|
109
|
+
@generated_flake_content
|
|
110
|
+
else
|
|
111
|
+
raise ConfCtl::Error, 'flake.nix missing; run flake migration first'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
edits = {}
|
|
115
|
+
override_inputs = {}
|
|
116
|
+
override_revs = {}
|
|
117
|
+
set_ops = []
|
|
118
|
+
|
|
119
|
+
module_paths.each do |path|
|
|
120
|
+
relative = path.sub(%r{\A#{Regexp.escape(cluster_dir)}/}, '')
|
|
121
|
+
machine = File.dirname(relative)
|
|
122
|
+
next if machine == '.'
|
|
123
|
+
|
|
124
|
+
content = File.read(path)
|
|
125
|
+
updated = content.gsub(/\bswpins\.channels\b/, 'inputs.channels')
|
|
126
|
+
|
|
127
|
+
overrides_json = load_machine_overrides(machine)
|
|
128
|
+
overrides = {}
|
|
129
|
+
overrides_json.keys.map(&:to_s).sort.each do |role|
|
|
130
|
+
spec_entry = overrides_json[role]
|
|
131
|
+
spec = build_pin_spec(spec_entry, "machine #{machine} role #{role}")
|
|
132
|
+
input_name = sanitize_input_name("#{role}__#{machine}")
|
|
133
|
+
overrides[role.to_s] = { input: input_name, spec: spec }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if overrides.any?
|
|
137
|
+
mapping = overrides.transform_values { |v| v[:input] }
|
|
138
|
+
updated, status = insert_overrides(updated, mapping)
|
|
139
|
+
|
|
140
|
+
if status == :missing_anchor
|
|
141
|
+
msg = "Warning: could not find inputs.channels in cluster/#{machine}/module.nix; " \
|
|
142
|
+
'skipping overrides for this machine'
|
|
143
|
+
puts Rainbow(msg).yellow
|
|
144
|
+
else
|
|
145
|
+
overrides.each do |role, data|
|
|
146
|
+
input_name = data[:input]
|
|
147
|
+
spec = data[:spec]
|
|
148
|
+
identity = pin_identity(spec)
|
|
149
|
+
|
|
150
|
+
if override_inputs[input_name] && pin_identity(override_inputs[input_name]) != identity
|
|
151
|
+
raise ConfCtl::Error, "conflicting override input '#{input_name}' for machine #{machine}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
override_inputs[input_name] ||= spec
|
|
155
|
+
|
|
156
|
+
rev = spec[:rev]
|
|
157
|
+
next unless rev
|
|
158
|
+
|
|
159
|
+
if override_revs[input_name] && override_revs[input_name] != rev
|
|
160
|
+
raise ConfCtl::Error, "conflicting revisions for override input '#{input_name}'"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
override_revs[input_name] = rev
|
|
164
|
+
set_ops << [:set, "machine #{machine} role #{role} rev #{rev}"]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
edits[path] = updated if updated != content
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
new_flake_content, added_inputs = add_inputs_to_flake(flake_content, override_inputs)
|
|
173
|
+
edits[flake_path] = new_flake_content if added_inputs.any?
|
|
174
|
+
|
|
175
|
+
ops = edits.keys.sort.map { |path| [:edit, display_path(path)] }
|
|
176
|
+
ops.concat(set_ops)
|
|
177
|
+
|
|
178
|
+
return true if ops.empty? && override_inputs.empty?
|
|
179
|
+
return false unless confirm_step(ops, label: 'machine metadata migration', required: true)
|
|
180
|
+
return true if dry_run?
|
|
181
|
+
|
|
182
|
+
edits.each do |path, content|
|
|
183
|
+
write_atomic(path, content)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if override_revs.any? && !File.exist?(File.join(conf_dir, 'flake.lock'))
|
|
187
|
+
raise ConfCtl::Error, 'flake.lock missing; run nix flake lock first'
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
apply_input_revisions(override_revs)
|
|
191
|
+
true
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def imports
|
|
195
|
+
step('Imports')
|
|
196
|
+
|
|
197
|
+
matches = scan_legacy_imports
|
|
198
|
+
if matches.empty?
|
|
199
|
+
puts 'No legacy NIX_PATH imports detected.'
|
|
200
|
+
return true
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
puts Rainbow('Legacy NIX_PATH imports detected:').yellow
|
|
204
|
+
matches.each do |m|
|
|
205
|
+
puts " #{m[:path]}:#{m[:line]}: #{m[:content]}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
puts
|
|
209
|
+
puts 'Pure flake evaluation will fail unless these imports are migrated or legacy mode is enabled.'
|
|
210
|
+
|
|
211
|
+
change = legacy_confctl_change
|
|
212
|
+
if change.nil?
|
|
213
|
+
puts 'Compatibility options already set in configs/confctl.nix.'
|
|
214
|
+
return true
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
op(:edit, 'configs/confctl.nix')
|
|
218
|
+
proceed = ask_confirmation { puts 'Enable compatibility mode in configs/confctl.nix?' }
|
|
219
|
+
return true unless proceed
|
|
220
|
+
return true if dry_run?
|
|
221
|
+
|
|
222
|
+
write_confctl_change(change)
|
|
223
|
+
true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def clean
|
|
227
|
+
step('Clean')
|
|
228
|
+
|
|
229
|
+
flake_path = File.join(conf_dir, 'flake.nix')
|
|
230
|
+
unless File.exist?(flake_path) || (dry_run? && @generated_flake_content)
|
|
231
|
+
raise ConfCtl::Error, 'flake.nix missing; refusing to remove swpins artifacts'
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
ops = []
|
|
235
|
+
swpins_config = File.join(conf_dir, 'configs', 'swpins.nix')
|
|
236
|
+
swpins_dir = File.join(conf_dir, 'swpins')
|
|
237
|
+
|
|
238
|
+
ops << [:delete, 'configs/swpins.nix'] if File.exist?(swpins_config)
|
|
239
|
+
ops << [:delete, 'swpins/'] if Dir.exist?(swpins_dir)
|
|
240
|
+
|
|
241
|
+
if ops.any?
|
|
242
|
+
return true unless confirm_step(ops, label: 'swpins cleanup', required: false)
|
|
243
|
+
return true if dry_run?
|
|
244
|
+
|
|
245
|
+
FileUtils.rm_f(swpins_config)
|
|
246
|
+
FileUtils.rm_rf(swpins_dir)
|
|
247
|
+
else
|
|
248
|
+
puts 'No swpins artifacts to remove.'
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
shell_path = File.join(conf_dir, 'shell.nix')
|
|
252
|
+
return true unless File.exist?(shell_path)
|
|
253
|
+
|
|
254
|
+
op(:delete, 'shell.nix')
|
|
255
|
+
proceed = ask_confirmation { puts 'Remove shell.nix?' }
|
|
256
|
+
return true unless proceed
|
|
257
|
+
return true if dry_run?
|
|
258
|
+
|
|
259
|
+
FileUtils.rm_f(shell_path)
|
|
260
|
+
true
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
protected
|
|
264
|
+
|
|
265
|
+
def conf_dir
|
|
266
|
+
ConfCtl::ConfDir.path
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def step(title)
|
|
270
|
+
puts Rainbow("\n== #{title} ==").bright.cyan
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def op(kind, target)
|
|
274
|
+
color =
|
|
275
|
+
case kind
|
|
276
|
+
when :create then :green
|
|
277
|
+
when :edit then :yellow
|
|
278
|
+
when :delete then :red
|
|
279
|
+
when :run, :set then :blue
|
|
280
|
+
else :white
|
|
281
|
+
end
|
|
282
|
+
puts "#{Rainbow(kind.to_s.upcase.ljust(6)).public_send(color)} #{target}"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def dry_run?
|
|
286
|
+
opts[:'dry-run'] || opts[:dry_run]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def write_atomic(path, content)
|
|
290
|
+
tmp = "#{path}.new"
|
|
291
|
+
File.write(tmp, content)
|
|
292
|
+
File.rename(tmp, path)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def display_path(path)
|
|
296
|
+
prefix = "#{conf_dir}/"
|
|
297
|
+
path.start_with?(prefix) ? path.delete_prefix(prefix) : path
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def confirm_step(ops, label:, required:)
|
|
301
|
+
if ops.empty?
|
|
302
|
+
puts 'No changes.'
|
|
303
|
+
return true
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
ops.each { |kind, target| op(kind, target) }
|
|
307
|
+
|
|
308
|
+
if ask_confirmation
|
|
309
|
+
true
|
|
310
|
+
else
|
|
311
|
+
action = required ? 'Aborting' : 'Skipping'
|
|
312
|
+
puts Rainbow("#{action} #{label}.").yellow
|
|
313
|
+
false
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def load_legacy_channels
|
|
318
|
+
legacy = ConfCtl::NixLegacy.new(conf_dir: conf_dir)
|
|
319
|
+
channels = legacy.list_swpins_channels
|
|
320
|
+
unless channels.is_a?(Hash)
|
|
321
|
+
raise ConfCtl::Error, 'unable to evaluate swpins channels from legacy config'
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
channels.transform_values { |v| v.is_a?(Hash) ? v : {} }
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def load_machine_overrides(machine)
|
|
328
|
+
safe_machine = machine.tr('/', ':')
|
|
329
|
+
path = File.join(conf_dir, 'swpins', 'cluster', "#{safe_machine}.json")
|
|
330
|
+
return {} unless File.exist?(path)
|
|
331
|
+
|
|
332
|
+
load_json(path, "swpins/cluster/#{safe_machine}.json")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def load_json(path, label)
|
|
336
|
+
JSON.parse(File.read(path))
|
|
337
|
+
rescue JSON::ParserError => e
|
|
338
|
+
raise ConfCtl::Error, "#{label} is not valid JSON: #{e.message}"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def build_pin_spec(entry, context)
|
|
342
|
+
raise ConfCtl::Error, "#{context}: missing swpin state" unless entry.is_a?(Hash)
|
|
343
|
+
|
|
344
|
+
type = entry['type']
|
|
345
|
+
nix_opts = entry['nix_options'] || {}
|
|
346
|
+
|
|
347
|
+
case type
|
|
348
|
+
when 'git', 'git-rev'
|
|
349
|
+
url = nix_opts['url']
|
|
350
|
+
raise ConfCtl::Error, "#{context}: missing nix_options.url" if url.nil? || url == ''
|
|
351
|
+
|
|
352
|
+
ref = normalize_ref(nix_opts.dig('update', 'ref'))
|
|
353
|
+
submodules = nix_opts['fetchSubmodules'] ? true : false
|
|
354
|
+
rev = entry.dig('state', 'rev')
|
|
355
|
+
if rev.nil? || rev == ''
|
|
356
|
+
raise ConfCtl::Error, "#{context}: missing state.rev; update swpins state first"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
{
|
|
360
|
+
type: type,
|
|
361
|
+
url: url,
|
|
362
|
+
ref: ref,
|
|
363
|
+
submodules: submodules,
|
|
364
|
+
rev: rev
|
|
365
|
+
}
|
|
366
|
+
when 'directory'
|
|
367
|
+
path = nix_opts['path']
|
|
368
|
+
raise ConfCtl::Error, "#{context}: missing nix_options.path" if path.nil? || path == ''
|
|
369
|
+
|
|
370
|
+
{
|
|
371
|
+
type: type,
|
|
372
|
+
path: path,
|
|
373
|
+
rev: entry.dig('state', 'rev')
|
|
374
|
+
}
|
|
375
|
+
else
|
|
376
|
+
raise ConfCtl::Error, "#{context}: unsupported swpin type '#{type}'"
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def build_channel_plan(legacy_channels, channel_state, core_spec)
|
|
381
|
+
used_names = {}
|
|
382
|
+
used_names['nixpkgs'] = pin_identity(core_spec)
|
|
383
|
+
used_names['confctl'] = :reserved
|
|
384
|
+
|
|
385
|
+
input_defs = {}
|
|
386
|
+
input_revs = {}
|
|
387
|
+
input_revs['nixpkgs'] = core_spec[:rev] if core_spec[:rev]
|
|
388
|
+
channels = {}
|
|
389
|
+
|
|
390
|
+
legacy_channels.keys.sort.each do |channel|
|
|
391
|
+
roles = legacy_channels[channel] || {}
|
|
392
|
+
channels[channel] = {}
|
|
393
|
+
|
|
394
|
+
roles.keys.map(&:to_s).sort.each do |role|
|
|
395
|
+
state = channel_state[channel][role]
|
|
396
|
+
unless state
|
|
397
|
+
raise ConfCtl::Error,
|
|
398
|
+
"swpins/channels/#{channel}.json missing role '#{role}'; update swpins state first"
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
spec = build_pin_spec(state, "channel #{channel} role #{role}")
|
|
402
|
+
identity = pin_identity(spec)
|
|
403
|
+
|
|
404
|
+
base = sanitize_input_name(role)
|
|
405
|
+
name = if used_names[base] && used_names[base] != identity
|
|
406
|
+
sanitize_input_name("#{role}__#{channel}")
|
|
407
|
+
else
|
|
408
|
+
base
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
name = ensure_unique_name(name, identity, used_names)
|
|
412
|
+
used_names[name] = identity
|
|
413
|
+
|
|
414
|
+
channels[channel][role] = name
|
|
415
|
+
|
|
416
|
+
unless input_defs.has_key?(name)
|
|
417
|
+
input_defs[name] = {
|
|
418
|
+
url: flake_url_for(spec),
|
|
419
|
+
flake: false
|
|
420
|
+
}
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
rev = spec[:rev]
|
|
424
|
+
next unless rev
|
|
425
|
+
|
|
426
|
+
if input_revs[name] && input_revs[name] != rev
|
|
427
|
+
raise ConfCtl::Error, "conflicting revisions for input '#{name}'"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
input_revs[name] = rev
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
input_defs.delete('nixpkgs')
|
|
435
|
+
{ input_defs: input_defs, channels: channels, input_revs: input_revs }
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def ensure_unique_name(base, identity, used_names)
|
|
439
|
+
name = base
|
|
440
|
+
idx = 2
|
|
441
|
+
while used_names.has_key?(name) && used_names[name] != identity
|
|
442
|
+
name = "#{base}__#{idx}"
|
|
443
|
+
idx += 1
|
|
444
|
+
end
|
|
445
|
+
name
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def pin_identity(spec)
|
|
449
|
+
[
|
|
450
|
+
spec[:type],
|
|
451
|
+
spec[:url],
|
|
452
|
+
spec[:path],
|
|
453
|
+
spec[:ref],
|
|
454
|
+
spec[:submodules],
|
|
455
|
+
spec[:rev]
|
|
456
|
+
]
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def sanitize_input_name(name)
|
|
460
|
+
sanitized = name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')
|
|
461
|
+
raise ConfCtl::Error, "unable to sanitize input name from '#{name}'" if sanitized.empty?
|
|
462
|
+
|
|
463
|
+
sanitized
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def normalize_ref(ref)
|
|
467
|
+
return nil if ref.nil? || ref == ''
|
|
468
|
+
|
|
469
|
+
ref.sub(%r{\Arefs/(heads|tags)/}, '')
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def github_repo_from_url(url)
|
|
473
|
+
m = url.match(%r{\Ahttps://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?\z})
|
|
474
|
+
return nil unless m
|
|
475
|
+
|
|
476
|
+
[m[1], m[2]]
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def flake_url_for(spec)
|
|
480
|
+
case spec[:type]
|
|
481
|
+
when 'directory'
|
|
482
|
+
"path:#{spec[:path]}"
|
|
483
|
+
when 'git', 'git-rev'
|
|
484
|
+
url = spec[:url].to_s.sub(/\Agit\+/, '')
|
|
485
|
+
ref = spec[:ref]
|
|
486
|
+
submodules = spec[:submodules]
|
|
487
|
+
|
|
488
|
+
gh = github_repo_from_url(url)
|
|
489
|
+
if gh && !submodules
|
|
490
|
+
base = "github:#{gh[0]}/#{gh[1]}"
|
|
491
|
+
return ref ? "#{base}/#{ref}" : base
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
git_url = "git+#{url}"
|
|
495
|
+
git_url = append_query(git_url, 'ref', ref) if ref && ref != ''
|
|
496
|
+
git_url = append_query(git_url, 'submodules', '1') if submodules
|
|
497
|
+
git_url
|
|
498
|
+
else
|
|
499
|
+
raise ConfCtl::Error, "unsupported swpin type '#{spec[:type]}'"
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def append_query(url, key, value)
|
|
504
|
+
separator = url.include?('?') ? '&' : '?'
|
|
505
|
+
"#{url}#{separator}#{key}=#{value}"
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def nix_attr_key(name)
|
|
509
|
+
str = name.to_s
|
|
510
|
+
if str.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
511
|
+
str
|
|
512
|
+
else
|
|
513
|
+
"\"#{nix_string(str)}\""
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def nix_attr_path(parts)
|
|
518
|
+
parts.map { |p| nix_attr_key(p) }.join('.')
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def nix_string(str)
|
|
522
|
+
str.to_s.gsub('\\', '\\\\').gsub('"', '\"')
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def render_flake(core_url, input_defs, channels)
|
|
526
|
+
lines = []
|
|
527
|
+
lines << '{'
|
|
528
|
+
lines << ' description = "confctl migrated swpins config";'
|
|
529
|
+
lines << ''
|
|
530
|
+
lines << ' inputs = {'
|
|
531
|
+
lines << ' confctl.url = "github:vpsfreecz/confctl";'
|
|
532
|
+
lines << " nixpkgs.url = \"#{nix_string(core_url)}\";"
|
|
533
|
+
lines << ''
|
|
534
|
+
lines << " #{INPUTS_BEGIN}"
|
|
535
|
+
lines.concat(render_input_defs(input_defs, ' '))
|
|
536
|
+
lines << " #{INPUTS_END}"
|
|
537
|
+
lines << ' };'
|
|
538
|
+
lines << ''
|
|
539
|
+
lines << ' outputs = inputs@{ self, confctl, ... }:'
|
|
540
|
+
lines << ' let'
|
|
541
|
+
lines << ' channels = {'
|
|
542
|
+
lines << " #{CHANNELS_BEGIN}"
|
|
543
|
+
lines.concat(render_channels(channels, ' '))
|
|
544
|
+
lines << " #{CHANNELS_END}"
|
|
545
|
+
lines << ' };'
|
|
546
|
+
lines << ' in'
|
|
547
|
+
lines << ' {'
|
|
548
|
+
lines << ' confctl = confctl.lib.mkConfctlOutputs {'
|
|
549
|
+
lines << ' confDir = ./.;'
|
|
550
|
+
lines << ' inherit inputs channels;'
|
|
551
|
+
lines << ' };'
|
|
552
|
+
lines << ''
|
|
553
|
+
lines << ' devShells.x86_64-linux.default = confctl.lib.mkConfigDevShell {'
|
|
554
|
+
lines << ' system = "x86_64-linux";'
|
|
555
|
+
lines << ' mode = "minimal";'
|
|
556
|
+
lines << ' };'
|
|
557
|
+
lines << ' };'
|
|
558
|
+
lines << '}'
|
|
559
|
+
lines << ''
|
|
560
|
+
lines.join("\n")
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def render_input_defs(input_defs, indent)
|
|
564
|
+
lines = []
|
|
565
|
+
input_defs.keys.sort.each do |name|
|
|
566
|
+
spec = input_defs[name]
|
|
567
|
+
key = nix_attr_key(name)
|
|
568
|
+
lines << "#{indent}#{key} = {"
|
|
569
|
+
lines << "#{indent} url = \"#{nix_string(spec[:url])}\";"
|
|
570
|
+
lines << "#{indent} flake = false;" if spec[:flake] == false
|
|
571
|
+
lines << "#{indent}};"
|
|
572
|
+
lines << ''
|
|
573
|
+
end
|
|
574
|
+
lines
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def render_channels(channels, indent)
|
|
578
|
+
lines = []
|
|
579
|
+
channels.keys.sort.each do |channel|
|
|
580
|
+
roles = channels[channel] || {}
|
|
581
|
+
lines << "#{indent}#{nix_attr_key(channel)} = {"
|
|
582
|
+
roles.keys.sort.each do |role|
|
|
583
|
+
input_name = roles[role]
|
|
584
|
+
lines << "#{indent} #{nix_attr_key(role)} = \"#{nix_string(input_name)}\";"
|
|
585
|
+
end
|
|
586
|
+
lines << "#{indent}};"
|
|
587
|
+
lines << ''
|
|
588
|
+
end
|
|
589
|
+
lines
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def insert_overrides(content, overrides)
|
|
593
|
+
return [content, :no_overrides] if overrides.empty?
|
|
594
|
+
|
|
595
|
+
lines = content.lines
|
|
596
|
+
idx = lines.index { |line| line.match?(/\binputs\.channels\b/) && !line.lstrip.start_with?('#') }
|
|
597
|
+
return [content, :missing_anchor] unless idx
|
|
598
|
+
|
|
599
|
+
indent = lines[idx][/^\s*/]
|
|
600
|
+
new_lines = []
|
|
601
|
+
|
|
602
|
+
overrides.each do |role, input_name|
|
|
603
|
+
next if override_line_present?(content, role)
|
|
604
|
+
|
|
605
|
+
attr_path = nix_attr_path(%w[inputs overrides] + [role])
|
|
606
|
+
new_lines << "#{indent}#{attr_path} = \"#{nix_string(input_name)}\";\n"
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
return [content, :already_present] if new_lines.empty?
|
|
610
|
+
|
|
611
|
+
lines.insert(idx + 1, *new_lines)
|
|
612
|
+
[lines.join, :inserted]
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def override_line_present?(content, role)
|
|
616
|
+
if role.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
617
|
+
content.match?(/\binputs\.overrides\.#{Regexp.escape(role)}\b/)
|
|
618
|
+
else
|
|
619
|
+
quoted = Regexp.escape("\"#{role}\"")
|
|
620
|
+
content.match?(/inputs\.overrides\.#{quoted}/)
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def add_inputs_to_flake(content, inputs)
|
|
625
|
+
return [content, []] if inputs.empty?
|
|
626
|
+
|
|
627
|
+
lines = content.lines
|
|
628
|
+
begin_idx = lines.index { |line| line.include?(INPUTS_BEGIN) }
|
|
629
|
+
end_idx = lines.index { |line| line.include?(INPUTS_END) }
|
|
630
|
+
|
|
631
|
+
unless begin_idx && end_idx && begin_idx < end_idx
|
|
632
|
+
raise ConfCtl::Error, 'flake.nix missing inputs marker block'
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
marker_indent = lines[begin_idx][/^\s*/]
|
|
636
|
+
entry_indent = marker_indent
|
|
637
|
+
lines[(begin_idx + 1)...end_idx].each do |line|
|
|
638
|
+
stripped = line.strip
|
|
639
|
+
next if stripped.empty? || stripped.start_with?('#')
|
|
640
|
+
|
|
641
|
+
entry_indent = line[/^\s*/]
|
|
642
|
+
break
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
existing = extract_input_names(lines[(begin_idx + 1)...end_idx], entry_indent)
|
|
646
|
+
|
|
647
|
+
new_names = inputs.keys.reject { |n| existing.include?(n) }.sort
|
|
648
|
+
return [content, []] if new_names.empty?
|
|
649
|
+
|
|
650
|
+
new_lines = []
|
|
651
|
+
new_names.each do |name|
|
|
652
|
+
spec = inputs[name]
|
|
653
|
+
key = nix_attr_key(name)
|
|
654
|
+
new_lines << "#{entry_indent}#{key} = {"
|
|
655
|
+
new_lines << "#{entry_indent} url = \"#{nix_string(flake_url_for(spec))}\";"
|
|
656
|
+
new_lines << "#{entry_indent} flake = false;"
|
|
657
|
+
new_lines << "#{entry_indent}};"
|
|
658
|
+
new_lines << ''
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
lines.insert(end_idx, *new_lines)
|
|
662
|
+
[lines.join, new_names]
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def extract_input_names(lines, entry_indent)
|
|
666
|
+
names = []
|
|
667
|
+
lines.each do |line|
|
|
668
|
+
stripped = line.strip
|
|
669
|
+
next if stripped.empty? || stripped.start_with?('#')
|
|
670
|
+
next unless line.start_with?(entry_indent)
|
|
671
|
+
next if line.start_with?("#{entry_indent} ")
|
|
672
|
+
|
|
673
|
+
match = stripped.match(/^([^=\s]+)\s*=\s*/)
|
|
674
|
+
match ||= stripped.match(/^([^=\s]+)\.url\s*=/)
|
|
675
|
+
next unless match
|
|
676
|
+
|
|
677
|
+
name = unquote(match[1])
|
|
678
|
+
names << name
|
|
679
|
+
end
|
|
680
|
+
names
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def unquote(str)
|
|
684
|
+
return str unless str.start_with?('"') && str.end_with?('"')
|
|
685
|
+
|
|
686
|
+
str[1..-2]
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def scan_legacy_imports
|
|
690
|
+
matches = []
|
|
691
|
+
dirs = %w[cluster configs modules environments data overlays]
|
|
692
|
+
dirs.each do |dir|
|
|
693
|
+
base = File.join(conf_dir, dir)
|
|
694
|
+
next unless Dir.exist?(base)
|
|
695
|
+
|
|
696
|
+
Dir.glob(File.join(base, '**', '*.nix')).each do |path|
|
|
697
|
+
File.readlines(path).each_with_index do |line, idx|
|
|
698
|
+
next unless line.match?(/<[^>\n]+>/)
|
|
699
|
+
|
|
700
|
+
matches << {
|
|
701
|
+
path: display_path(path),
|
|
702
|
+
line: idx + 1,
|
|
703
|
+
content: line.rstrip
|
|
704
|
+
}
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
matches
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def legacy_confctl_change
|
|
712
|
+
path = File.join(conf_dir, 'configs', 'confctl.nix')
|
|
713
|
+
|
|
714
|
+
if File.exist?(path)
|
|
715
|
+
content = File.read(path)
|
|
716
|
+
needs_impure = !content.match?(/\bconfctl\.nix\.impureEval\s*=/)
|
|
717
|
+
needs_legacy = !content.match?(/\bconfctl\.nix\.legacyNixPath\s*=/)
|
|
718
|
+
return nil unless needs_impure || needs_legacy
|
|
719
|
+
|
|
720
|
+
{
|
|
721
|
+
path: path,
|
|
722
|
+
content: content,
|
|
723
|
+
needs_impure: needs_impure,
|
|
724
|
+
needs_legacy: needs_legacy,
|
|
725
|
+
create: false
|
|
726
|
+
}
|
|
727
|
+
else
|
|
728
|
+
{
|
|
729
|
+
path: path,
|
|
730
|
+
content: nil,
|
|
731
|
+
needs_impure: true,
|
|
732
|
+
needs_legacy: true,
|
|
733
|
+
create: true
|
|
734
|
+
}
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def write_confctl_change(change)
|
|
739
|
+
path = change[:path]
|
|
740
|
+
|
|
741
|
+
if change[:create]
|
|
742
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
743
|
+
content = <<~NIX
|
|
744
|
+
{ ... }:
|
|
745
|
+
{
|
|
746
|
+
# confctl migrate swpins-to-flakes: legacy NIX_PATH compatibility
|
|
747
|
+
confctl.nix.impureEval = true;
|
|
748
|
+
confctl.nix.legacyNixPath = true;
|
|
749
|
+
}
|
|
750
|
+
NIX
|
|
751
|
+
write_atomic(path, content)
|
|
752
|
+
return
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
lines = change[:content].lines
|
|
756
|
+
close_idx = lines.rindex { |line| line.strip == '}' }
|
|
757
|
+
close_idx ||= lines.rindex { |line| line.strip == '};' }
|
|
758
|
+
raise ConfCtl::Error, 'unable to edit configs/confctl.nix' unless close_idx
|
|
759
|
+
|
|
760
|
+
indent = lines[close_idx][/^\s*/]
|
|
761
|
+
inner_indent = "#{indent} "
|
|
762
|
+
|
|
763
|
+
insert_lines = []
|
|
764
|
+
insert_lines << "#{inner_indent}# confctl migrate swpins-to-flakes: legacy NIX_PATH compatibility\n"
|
|
765
|
+
insert_lines << "#{inner_indent}confctl.nix.impureEval = true;\n" if change[:needs_impure]
|
|
766
|
+
insert_lines << "#{inner_indent}confctl.nix.legacyNixPath = true;\n" if change[:needs_legacy]
|
|
767
|
+
|
|
768
|
+
lines.insert(close_idx, *insert_lines)
|
|
769
|
+
write_atomic(path, lines.join)
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def apply_input_revisions(input_revs)
|
|
773
|
+
with_tracked_flake do
|
|
774
|
+
input_revs.keys.sort.each do |input|
|
|
775
|
+
rev = input_revs[input]
|
|
776
|
+
next if rev.nil? || rev == ''
|
|
777
|
+
|
|
778
|
+
ConfCtl::Inputs::Setter.run!(
|
|
779
|
+
conf_dir: conf_dir,
|
|
780
|
+
inputs: [input],
|
|
781
|
+
rev: rev,
|
|
782
|
+
commit: false,
|
|
783
|
+
changelog: false,
|
|
784
|
+
downgrade: false,
|
|
785
|
+
editor: false
|
|
786
|
+
)
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def run_nix_flake_lock!
|
|
792
|
+
with_tracked_flake do
|
|
793
|
+
cmd = ConfCtl::SystemCommand.new
|
|
794
|
+
extra_experimental = false
|
|
795
|
+
|
|
796
|
+
loop do
|
|
797
|
+
args = ['nix']
|
|
798
|
+
if extra_experimental
|
|
799
|
+
args << '--extra-experimental-features' << 'nix-command'
|
|
800
|
+
args << '--extra-experimental-features' << 'flakes'
|
|
801
|
+
end
|
|
802
|
+
args << 'flake' << 'lock'
|
|
803
|
+
|
|
804
|
+
begin
|
|
805
|
+
Dir.chdir(conf_dir) { cmd.run(*args) }
|
|
806
|
+
break
|
|
807
|
+
rescue TTY::Command::ExitError => e
|
|
808
|
+
if !extra_experimental && experimental_error?(e.message)
|
|
809
|
+
extra_experimental = true
|
|
810
|
+
next
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
raise
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def experimental_error?(message)
|
|
820
|
+
message.match?(/experimental/i) && message.match?(/nix-command|flakes/i)
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def with_tracked_flake
|
|
824
|
+
flake_path = File.join(conf_dir, 'flake.nix')
|
|
825
|
+
return yield unless File.exist?(flake_path)
|
|
826
|
+
return yield unless git_repo?
|
|
827
|
+
|
|
828
|
+
added = false
|
|
829
|
+
|
|
830
|
+
unless git_tracked_file?('flake.nix')
|
|
831
|
+
git_add_intent('flake.nix')
|
|
832
|
+
added = true
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
yield
|
|
836
|
+
ensure
|
|
837
|
+
git_reset_file('flake.nix') if added
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
def git_repo?
|
|
841
|
+
cmd = ConfCtl::SystemCommand.new
|
|
842
|
+
Dir.chdir(conf_dir) { cmd.run('git', 'rev-parse', '--is-inside-work-tree') }
|
|
843
|
+
true
|
|
844
|
+
rescue TTY::Command::ExitError
|
|
845
|
+
false
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def git_tracked_file?(path)
|
|
849
|
+
cmd = ConfCtl::SystemCommand.new
|
|
850
|
+
Dir.chdir(conf_dir) { cmd.run('git', 'ls-files', '--error-unmatch', path) }
|
|
851
|
+
true
|
|
852
|
+
rescue TTY::Command::ExitError
|
|
853
|
+
false
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def git_add_intent(path)
|
|
857
|
+
cmd = ConfCtl::SystemCommand.new
|
|
858
|
+
Dir.chdir(conf_dir) { cmd.run('git', 'add', '--intent-to-add', path) }
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def git_reset_file(path)
|
|
862
|
+
cmd = ConfCtl::SystemCommand.new
|
|
863
|
+
Dir.chdir(conf_dir) { cmd.run('git', 'reset', '-q', '--', path) }
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
end
|