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 =
%w[
  BUNDLE_BIN_PATH
  BUNDLE_FROZEN
  BUNDLER_VERSION
  RUBYOPT
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(root:, command_runner:) ⇒ LockfileReset

Returns a new instance of LockfileReset.



32
33
34
35
# File 'lib/kettle/dev/lockfile_reset.rb', line 32

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

Instance Method Details

#diagnostics(path) ⇒ Object



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

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
  diagnostics
end

#display_path(path) ⇒ Object



261
262
263
# File 'lib/kettle/dev/lockfile_reset.rb', line 261

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

#dynamic_local_path_envObject



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

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



199
200
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
# File 'lib/kettle/dev/lockfile_reset.rb', line 199

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



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

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)


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

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)


178
179
180
# File 'lib/kettle/dev/lockfile_reset.rb', line 178

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

#local_path_env_value?(value) ⇒ Boolean

Returns:

  • (Boolean)


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

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



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

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:



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

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



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

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:



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

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



157
158
159
# File 'lib/kettle/dev/lockfile_reset.rb', line 157

def normalization_env
  DEFAULT_DISABLED_ENV.merge(dynamic_local_path_env)
end

#normalization_needed?(path) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#path_dependency_gems(path) ⇒ Object



248
249
250
251
252
253
# File 'lib/kettle/dev/lockfile_reset.rb', line 248

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



241
242
243
244
245
246
# File 'lib/kettle/dev/lockfile_reset.rb', line 241

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)


146
147
148
# File 'lib/kettle/dev/lockfile_reset.rb', line 146

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

#reset(target) ⇒ Object

Raises:



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

def reset(target)
  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



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

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



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

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



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

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

#self_path_source_gems(path) ⇒ Object



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

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