Class: Kettle::Dev::LockfileReset

Inherits:
Object
  • Object
show all
Defined in:
lib/kettle/dev/lockfile_reset.rb

Constant Summary collapse

DEFAULT_DISABLED_ENV =
{
  "KETTLE_DEV_DEV" => "false",
  "K_JEM_TEMPLATING" => "false",
  "STRUCTUREDMERGE_DEV" => "false",
  "TREE_SITTER_LANGUAGE_PACK_DEV" => "false",
  "RUBOCOP_LTS_LOCAL" => "false",
  "GALTZO_FLOSS_DEV" => "false",
  "UR_BRAIN_DEV" => "false"
}.freeze
RELEASE_LOCKFILES_TARGET =
"release-lockfiles"
SUPPORTED_TARGETS =
["Gemfile.lock", "Appraisal.root.gemfile.lock", RELEASE_LOCKFILES_TARGET].freeze
UNBUNDLED_ENV_KEYS =
(BundlerEnvGuard::RESET_ENV_KEYS + %w[
  RUBYLIB
  RUBYOPT
]).freeze

Instance Method Summary collapse

Constructor Details

#initialize(root:, command_runner:) ⇒ LockfileReset

Returns a new instance of LockfileReset.



29
30
31
32
# File 'lib/kettle/dev/lockfile_reset.rb', line 29

def initialize(root:, command_runner:)
  @root = root
  @command_runner = command_runner
end

Instance Method Details

#diagnostics(path) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/kettle/dev/lockfile_reset.rb', line 101

def diagnostics(path)
  diagnostics = []
  local_path_remote_lines(path).each do |line_number|
    diagnostics << "#{display_path(path)} has local path remote at line #{line_number}"
  end
  empty_registry_checksums(path).each do |name, version, line_number|
    diagnostics << "#{display_path(path)} CHECKSUMS has no sha256 for #{name} #{version} at line #{line_number}"
  end
  unreleased_workspace_registry_specs(path).each do |name, version, line_number|
    diagnostics << "#{display_path(path)} locks local workspace gem #{name} #{version} as a registry gem, but that version is not resolvable from the configured gem source at line #{line_number}"
  end
  diagnostics
end

#display_path(path) ⇒ Object



276
277
278
# File 'lib/kettle/dev/lockfile_reset.rb', line 276

def display_path(path)
  Kettle::Dev.display_path(path)
end

#dynamic_local_path_envObject



163
164
165
166
167
168
169
170
# File 'lib/kettle/dev/lockfile_reset.rb', line 163

def dynamic_local_path_env
  ENV.each_with_object({}) do |(key, value), env|
    next unless key.end_with?("_DEV", "_LOCAL")
    next unless local_path_env_value?(value)

    env[key] = "false"
  end
end

#empty_registry_checksums(path) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/kettle/dev/lockfile_reset.rb', line 201

def empty_registry_checksums(path)
  path_gems = path_source_gems(path) | path_dependency_gems(path) | self_path_source_gems(path)
  in_checksums = false
  entries = File.readlines(path).filter_map.with_index(1) do |line, index|
    stripped = line.strip
    if stripped == "CHECKSUMS"
      in_checksums = true
      next
    end
    next unless in_checksums
    next if stripped.empty?
    next if stripped == "BUNDLED WITH"

    match = stripped.match(/\A([A-Za-z0-9_.-]+) \(([^)]+)\)(?:\s+(sha256=.*))?\z/)
    next unless match

    name = match[1]
    checksum = match[3].to_s
    next unless checksum.empty?
    next if path_gems.include?(name)

    [name, match[2], index]
  end
  return [] unless has_any_sha_checksum?(path)

  entries
end

#gemfile_for_lockfile(path) ⇒ Object



152
153
154
155
156
157
# File 'lib/kettle/dev/lockfile_reset.rb', line 152

def gemfile_for_lockfile(path)
  basename = File.basename(path)
  return File.join(root, "Gemfile") if basename == "Gemfile.lock"

  path.delete_suffix(".lock")
end

#has_any_sha_checksum?(path) ⇒ Boolean

Returns:

  • (Boolean)


242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/kettle/dev/lockfile_reset.rb', line 242

def has_any_sha_checksum?(path)
  in_checksums = false
  File.readlines(path).any? do |line|
    stripped = line.strip
    if stripped == "CHECKSUMS"
      in_checksums = true
      next false
    end
    next false unless in_checksums

    stripped.include?("sha256=")
  end
end

#has_local_path_remote?(path) ⇒ Boolean

Returns:

  • (Boolean)


180
181
182
# File 'lib/kettle/dev/lockfile_reset.rb', line 180

def has_local_path_remote?(path)
  !local_path_remote_lines(path).empty?
end

#local_path_env_value?(value) ⇒ Boolean

Returns:

  • (Boolean)


172
173
174
175
176
177
178
# File 'lib/kettle/dev/lockfile_reset.rb', line 172

def local_path_env_value?(value)
  text = value.to_s.strip
  return false if text.empty? || text.casecmp("false").zero?
  return true if text.start_with?("/", "./", "../", "~")

  text.include?(File::SEPARATOR)
end

#local_path_remote_lines(path) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/kettle/dev/lockfile_reset.rb', line 184

def local_path_remote_lines(path)
  in_path = false
  File.readlines(path).filter_map.with_index(1) do |line, index|
    stripped = line.strip
    if stripped.match?(/\A[A-Z][A-Z ]*\z/)
      in_path = stripped == "PATH"
      next
    end
    next unless in_path
    next unless stripped.start_with?("remote:")
    next if stripped == "remote: ."
    next unless stripped.start_with?("remote: /", "remote: ./", "remote: ../")

    index
  end
end

#lockfile_path_for(target) ⇒ Object

Raises:



123
124
125
126
127
128
# File 'lib/kettle/dev/lockfile_reset.rb', line 123

def lockfile_path_for(target)
  paths = lockfile_paths_for(target)
  raise Error, "reset target #{target.inspect} resolves to multiple lockfiles; use lockfile_paths_for" if paths.length != 1

  paths.first
end

#lockfile_pathsObject



115
116
117
118
119
120
121
# File 'lib/kettle/dev/lockfile_reset.rb', line 115

def lockfile_paths
  candidates = [
    File.join(root, "Gemfile.lock"),
    File.join(root, "Appraisal.root.gemfile.lock")
  ]
  candidates.select { |path| File.file?(path) }.sort
end

#lockfile_paths_for(target) ⇒ Object

Raises:



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/kettle/dev/lockfile_reset.rb', line 130

def lockfile_paths_for(target)
  normalized = target.to_s.strip
  raise Error, "reset requires TARGET" if normalized.empty?
  if release_lockfiles_target?(normalized)
    paths = lockfile_paths
    raise Error, "no release lockfiles found" if paths.empty?

    return paths
  end
  supported = SUPPORTED_TARGETS.reject { |candidate| candidate == RELEASE_LOCKFILES_TARGET }
  match = supported.find { |candidate| candidate.casecmp(normalized).zero? }
  unless match
    raise Error, "reset target #{normalized.inspect} is not supported; supported targets: #{SUPPORTED_TARGETS.join(", ")}"
  end

  [File.join(root, match)]
end

#normalization_envObject



159
160
161
# File 'lib/kettle/dev/lockfile_reset.rb', line 159

def normalization_env
  DEFAULT_DISABLED_ENV.merge(dynamic_local_path_env)
end

#normalization_needed?(path) ⇒ Boolean

Returns:

  • (Boolean)


97
98
99
# File 'lib/kettle/dev/lockfile_reset.rb', line 97

def normalization_needed?(path)
  has_local_path_remote?(path) || !empty_registry_checksums(path).empty? || !unreleased_workspace_registry_specs(path).empty?
end

#path_dependency_gems(path) ⇒ Object



263
264
265
266
267
268
# File 'lib/kettle/dev/lockfile_reset.rb', line 263

def path_dependency_gems(path)
  lockfile_parser(path).dependencies.each_value.each_with_object(Set.new) do |dependency, gems|
    source = dependency.source
    gems << dependency.name if path_source?(source) && !self_path_source?(source)
  end
end

#path_source_gems(path) ⇒ Object



256
257
258
259
260
261
# File 'lib/kettle/dev/lockfile_reset.rb', line 256

def path_source_gems(path)
  lockfile_parser(path).specs.each_with_object(Set.new) do |spec, gems|
    source = spec.source
    gems << spec.name if path_source?(source) && !self_path_source?(source)
  end
end

#release_lockfiles_target?(target) ⇒ Boolean

Returns:

  • (Boolean)


148
149
150
# File 'lib/kettle/dev/lockfile_reset.rb', line 148

def release_lockfiles_target?(target)
  target.to_s.strip.casecmp(RELEASE_LOCKFILES_TARGET).zero?
end

#reset(target) ⇒ Object

Raises:



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/kettle/dev/lockfile_reset.rb', line 34

def reset(target)
  BundlerEnvGuard.warn_unexpected_env!
  paths = lockfile_paths_for(target)
  force_full_update = release_lockfiles_target?(target)
  uninstall_unreleased_local_gems(paths) if force_full_update
  paths.each do |path|
    reset_lockfile!(path, full_update: force_full_update) if force_full_update || normalization_needed?(path)
  end
  if force_full_update && uninstall_unreleased_local_gems(paths)
    paths.each do |path|
      reset_lockfile!(path, full_update: true)
    end
  end
  diagnostics = paths.flat_map { |path| diagnostics(path) }
  raise Error, validation_message(target, diagnostics) unless diagnostics.empty?

  paths
end

#reset_command(path:, gemfile:, full_update: false) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/kettle/dev/lockfile_reset.rb', line 68

def reset_command(path:, gemfile:, full_update: false)
  update_gems = (full_update || has_local_path_remote?(path)) ? [] : reset_update_gems(path)
  env = normalization_env.merge(
    "BUNDLE_GEMFILE" => gemfile,
    "BUNDLE_LOCKFILE" => path
  )
  command = +"env"
  UNBUNDLED_ENV_KEYS.each do |key|
    command << " -u #{key}"
  end
  env.each do |key, value|
    command << " #{key}=#{Shellwords.escape(value)}"
  end
  command << " bundle lock"
  command << " --update"
  command << " #{update_gems.map { |gem_name| Shellwords.escape(gem_name) }.join(" ")}" unless update_gems.empty?
  command << " --add-checksums"
  command
end

#reset_lockfile!(path, full_update: false) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/kettle/dev/lockfile_reset.rb', line 53

def reset_lockfile!(path, full_update: false)
  gemfile = gemfile_for_lockfile(path)
  unless gemfile && File.file?(gemfile)
    warn("Cannot reset #{display_path(path)} because its Gemfile was not found.")
    return
  end

  command = reset_command(path: path, gemfile: gemfile, full_update: full_update)
  if full_update || has_local_path_remote?(path)
    rebuild_lockfile(path) { command_runner.call(command) }
  else
    command_runner.call(command)
  end
end

#reset_update_gems(path) ⇒ Object



88
89
90
91
92
93
94
95
# File 'lib/kettle/dev/lockfile_reset.rb', line 88

def reset_update_gems(path)
  (
    path_source_gems(path).to_a |
    path_dependency_gems(path).to_a |
    empty_registry_checksums(path).map(&:first) |
    unreleased_workspace_registry_specs(path).map(&:first)
  ).uniq.sort
end

#self_path_source_gems(path) ⇒ Object



270
271
272
273
274
# File 'lib/kettle/dev/lockfile_reset.rb', line 270

def self_path_source_gems(path)
  lockfile_parser(path).specs.each_with_object(Set.new) do |spec, gems|
    gems << spec.name if self_path_source?(spec.source)
  end
end

#unreleased_workspace_registry_specs(path) ⇒ Object



229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/kettle/dev/lockfile_reset.rb', line 229

def unreleased_workspace_registry_specs(path)
  local_names = local_workspace_gem_names
  return [] if local_names.empty?

  source_urls = registry_source_urls(path)
  registry_specs_with_lines(path).filter_map do |name, version, line_number|
    next unless local_names.include?(name)
    next if gem_source_version_available?(name, version, source_urls)

    [name, version, line_number]
  end
end