pray-cli 1.26.0 → 1.26.3
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/CHANGELOG.md +13 -0
- data/lib/pray/cli/commands/packages.rb +6 -1
- data/lib/pray/cli/helpers.rb +1 -1
- data/lib/pray/hashing.rb +2 -0
- data/lib/pray/package_spec.rb +0 -21
- data/lib/pray/package_spec_tree_hash.rb +35 -0
- data/lib/pray/render_dest.rb +2 -3
- data/lib/pray/render_reconcile.rb +16 -4
- data/lib/pray/resolve.rb +7 -7
- data/lib/pray/resolve_deps.rb +22 -1
- data/lib/pray/verify.rb +12 -6
- data/lib/pray/version.rb +2 -2
- data/lib/pray.rb +1 -0
- data/pray-cli.gemspec +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c518cdb4d98021a08d150ad7c0c20463d885b09c9f69b022f2fa596d9597269
|
|
4
|
+
data.tar.gz: 8e46339135b9a8cb554eb4b8d9f60510baa5eab1b1b730bbda70efdd227289f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b619b7f79ee252328c885ac0664270d29f00f81a720102a26f8a1af8d78c57e7f9245f6eb09e724a3ba1f19d482001c59a8153d0f2da534d64fe64cdc5cc314
|
|
7
|
+
data.tar.gz: bd6f8f108e1e8cab3e48060c09dff86b8dfc55cc5edaaed320fcb6d347f2b9ea90e208ceb5c2757136084b7da815f34b491ef7fffec3f78d7b70abfb530819d3
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.26.3 (2026-11-12)
|
|
6
|
+
|
|
7
|
+
- Allow `pray install` to refresh the lock when a managed span already matches the fresh render but the lock ideal is stale. Keep failing when the on-disk body matches neither, and name move-aside recovery instead of a force flag.
|
|
8
|
+
|
|
9
|
+
## 1.26.2 (2026-11-06)
|
|
10
|
+
|
|
11
|
+
- Reduce wall and CPU time for package resolve, tree hashing, and related install work without changing install results.
|
|
12
|
+
|
|
13
|
+
## 1.26.1 (2026-11-02)
|
|
14
|
+
|
|
15
|
+
- Show optional package referrals separately in `pray explain`, exclude them from dependency cycles and trees, and validate their constraints when another declaration selects the target.
|
|
16
|
+
- Keep drift, render checks, planning, and writes consistent for managed files that contain local content.
|
|
17
|
+
|
|
5
18
|
## 1.26.0 (2026-10-30)
|
|
6
19
|
|
|
7
20
|
- Add validated `related` references to prayspec parsing, rendering, and `pray explain`.
|
|
@@ -84,7 +84,12 @@ module Pray
|
|
|
84
84
|
end
|
|
85
85
|
lines << "source: #{package_source_summary(package)}"
|
|
86
86
|
lines << "exports: #{format_list(package.selected_exports)}"
|
|
87
|
-
|
|
87
|
+
required_dependencies = package.spec.dependencies.reject(&:optional).map(&:name)
|
|
88
|
+
optional_referrals = package.spec.dependencies.select(&:optional).map do |dependency|
|
|
89
|
+
"#{dependency.name} (#{dependency.constraint})"
|
|
90
|
+
end
|
|
91
|
+
lines << "dependencies: #{format_list(required_dependencies)}"
|
|
92
|
+
lines << "optional referrals: #{format_list(optional_referrals)}"
|
|
88
93
|
lines << "related prayers: #{format_list(package.spec.related)}"
|
|
89
94
|
lines << "tree hash: #{package.tree_hash}"
|
|
90
95
|
lines << "artifact hash: #{package.artifact_hash}"
|
data/lib/pray/cli/helpers.rb
CHANGED
|
@@ -152,7 +152,7 @@ module Pray
|
|
|
152
152
|
lines << "#{indent}#{package.declaration.name} #{package.spec.recorded_version}"
|
|
153
153
|
return unless ancestry.add?(package.declaration.name)
|
|
154
154
|
|
|
155
|
-
package.spec.dependencies.each do |dependency|
|
|
155
|
+
package.spec.dependencies.reject(&:optional).each do |dependency|
|
|
156
156
|
resolved = package_map[dependency.name]
|
|
157
157
|
if resolved
|
|
158
158
|
if ancestry.include?(resolved.declaration.name)
|
data/lib/pray/hashing.rb
CHANGED
data/lib/pray/package_spec.rb
CHANGED
|
@@ -54,27 +54,6 @@ module Pray
|
|
|
54
54
|
copy.dependencies = dependencies.sort_by { |dependency| [dependency.name, dependency.constraint, dependency.optional] }
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
|
-
|
|
58
|
-
def tree_hash_for_root(root)
|
|
59
|
-
validate_relative_paths!
|
|
60
|
-
file_bytes = {}
|
|
61
|
-
files.each do |file|
|
|
62
|
-
path = File.join(root, file)
|
|
63
|
-
raise Error.integrity("package file missing: #{file}") unless File.exist?(path)
|
|
64
|
-
raise Error.integrity("package file is a directory: #{file}") if File.directory?(path)
|
|
65
|
-
|
|
66
|
-
file_bytes[file] = File.binread(path)
|
|
67
|
-
end
|
|
68
|
-
self.class.tree_hash_from_file_bytes(file_bytes)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.tree_hash_from_file_bytes(file_bytes)
|
|
72
|
-
entries = file_bytes.map { |path, bytes| [path, Hashing.sha256_prefixed(bytes)] }.sort_by(&:first)
|
|
73
|
-
serialized = entries.map do |path, hash|
|
|
74
|
-
"file\0regular\0#{path}\0#{hash}\n"
|
|
75
|
-
end.join
|
|
76
|
-
Hashing.sha256_prefixed(serialized)
|
|
77
|
-
end
|
|
78
57
|
end
|
|
79
58
|
|
|
80
59
|
module PackageSpecParser
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
class PackageSpec
|
|
7
|
+
def tree_hash_for_root(root)
|
|
8
|
+
validate_relative_paths!
|
|
9
|
+
file_bytes = {}
|
|
10
|
+
files.each do |file|
|
|
11
|
+
path = File.join(root, file)
|
|
12
|
+
raise Error.integrity("package file missing: #{file}") unless File.exist?(path)
|
|
13
|
+
raise Error.integrity("package file is a directory: #{file}") if File.directory?(path)
|
|
14
|
+
|
|
15
|
+
file_bytes[file] = File.binread(path)
|
|
16
|
+
end
|
|
17
|
+
self.class.tree_hash_from_file_bytes(file_bytes)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.tree_hash_from_file_bytes(file_bytes)
|
|
21
|
+
# Stream the same bytes the prior string builder emitted so the digest stays identical.
|
|
22
|
+
digest = Digest::SHA256.new
|
|
23
|
+
file_bytes.keys.sort.each do |path|
|
|
24
|
+
bytes = file_bytes.fetch(path)
|
|
25
|
+
digest << "file\0regular\0"
|
|
26
|
+
digest << path
|
|
27
|
+
digest << "\0"
|
|
28
|
+
digest << "sha256:"
|
|
29
|
+
digest << Digest::SHA256.hexdigest(bytes)
|
|
30
|
+
digest << "\n"
|
|
31
|
+
end
|
|
32
|
+
Hashing.prefixed_hex_digest(digest.digest)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/pray/render_dest.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
-
require "pathname"
|
|
5
4
|
|
|
6
5
|
module Pray
|
|
7
6
|
module RenderDest
|
|
@@ -157,7 +156,7 @@ module Pray
|
|
|
157
156
|
unless kind == :regular
|
|
158
157
|
raise Error.render("refusing to write `#{display}`; destination is not a regular file")
|
|
159
158
|
end
|
|
160
|
-
|
|
159
|
+
RenderReconcile.reconcile_content(decode_utf8(read_regular_bytes(path, display), display), fresh, retired)
|
|
161
160
|
end
|
|
162
161
|
|
|
163
162
|
def write_rendered_content(path, display, fresh, retired = {})
|
|
@@ -165,7 +164,7 @@ module Pray
|
|
|
165
164
|
|
|
166
165
|
open_regular(path, display, File::RDWR) do |file|
|
|
167
166
|
existing = decode_utf8(read_destination_bytes(file, display), display)
|
|
168
|
-
content =
|
|
167
|
+
content = RenderReconcile.reconcile_content(existing, fresh, retired)
|
|
169
168
|
return if Transaction.replace(path, existing.b, content.b)
|
|
170
169
|
|
|
171
170
|
file.rewind
|
|
@@ -4,6 +4,10 @@ module Pray
|
|
|
4
4
|
module RenderReconcile
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
+
def reconcile_content(existing, fresh, retired = {})
|
|
8
|
+
RenderPatch.patch_rendered_content(existing, fresh, retired)
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
def current?(path, display, fresh, retired = {})
|
|
8
12
|
kind = RenderDest.destination_kind(path)
|
|
9
13
|
return false if kind == :missing
|
|
@@ -13,7 +17,7 @@ module Pray
|
|
|
13
17
|
raise Error.render("refusing to read `#{display}`; destination is not a regular file")
|
|
14
18
|
end
|
|
15
19
|
existing = RenderDest.decode_utf8(RenderDest.read_regular_bytes(path, display), display)
|
|
16
|
-
existing ==
|
|
20
|
+
existing == reconcile_content(existing, fresh, retired)
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def retired_span_checksums(previous_lockfile, target)
|
|
@@ -27,6 +31,9 @@ module Pray
|
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def reject_managed_span_conflicts!(project, rendered, previous_lockfile)
|
|
34
|
+
fresh_ideals = rendered.each_with_object({}) do |target, ideals|
|
|
35
|
+
target.managed_spans.each { |span| ideals[span.id] = span.ideal_checksum }
|
|
36
|
+
end
|
|
30
37
|
active_by_target = rendered.to_h do |target|
|
|
31
38
|
[target.path, target.managed_spans.to_h { |span| [span.id, true] }]
|
|
32
39
|
end
|
|
@@ -43,11 +50,16 @@ module Pray
|
|
|
43
50
|
bodies = RenderPatch.managed_bodies(RenderPatch.split_segments(content))
|
|
44
51
|
spans.each do |span|
|
|
45
52
|
next unless active[span.id] && bodies.key?(span.id)
|
|
46
|
-
|
|
53
|
+
|
|
54
|
+
checksum = Hashing.checksum_managed_span_content(bodies[span.id])
|
|
55
|
+
next if checksum == span.ideal_checksum
|
|
56
|
+
next if checksum == fresh_ideals[span.id]
|
|
47
57
|
|
|
48
58
|
raise Error.render(
|
|
49
|
-
"conflict: managed span `#{span.id}` in `#{target_path}`
|
|
50
|
-
"(`#{span.package}::#{span.export}`)"
|
|
59
|
+
"conflict: managed span `#{span.id}` in `#{target_path}` differs from lock and from " \
|
|
60
|
+
"fresh render (`#{span.package}::#{span.export}`).\n" \
|
|
61
|
+
"Move the destination aside and rerun `pray install`, or restore the managed block " \
|
|
62
|
+
"to the locked bytes."
|
|
51
63
|
)
|
|
52
64
|
end
|
|
53
65
|
end
|
data/lib/pray/resolve.rb
CHANGED
|
@@ -100,6 +100,7 @@ module Pray
|
|
|
100
100
|
raise Error.resolution(local_errors.join("\n")) unless local_errors.empty?
|
|
101
101
|
|
|
102
102
|
ResolveDeps.reject_dependency_cycles(packages)
|
|
103
|
+
ResolveDeps.validate_optional_referrals(packages)
|
|
103
104
|
source_revisions = git_sources.revisions
|
|
104
105
|
|
|
105
106
|
ResolvedProject.new(
|
|
@@ -276,18 +277,17 @@ module Pray
|
|
|
276
277
|
end
|
|
277
278
|
|
|
278
279
|
def find_prayspec_file(root)
|
|
279
|
-
|
|
280
|
+
found = nil
|
|
280
281
|
Dir.each_child(root) do |entry|
|
|
281
282
|
next unless File.extname(entry) == ".prayspec"
|
|
282
283
|
|
|
283
284
|
path = File.join(root, entry)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
when 0 then raise Error.resolution("no prayspec file found in #{root.inspect}")
|
|
289
|
-
else raise Error.resolution("multiple prayspec files found in #{root.inspect}")
|
|
285
|
+
next unless File.file?(path)
|
|
286
|
+
raise Error.resolution("multiple prayspec files found in #{root.inspect}") if found
|
|
287
|
+
|
|
288
|
+
found = path
|
|
290
289
|
end
|
|
290
|
+
found || raise(Error.resolution("no prayspec file found in #{root.inspect}"))
|
|
291
291
|
end
|
|
292
292
|
|
|
293
293
|
def source_map(sources)
|
data/lib/pray/resolve_deps.rb
CHANGED
|
@@ -5,14 +5,35 @@ module Pray
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def reject_dependency_cycles(packages)
|
|
8
|
+
return if packages.all? { |package| package.spec.dependencies.all?(&:optional) }
|
|
9
|
+
|
|
8
10
|
edges = {}
|
|
9
11
|
packages.each do |package|
|
|
10
|
-
edges[package.declaration.name] = package.spec.dependencies.map(&:name)
|
|
12
|
+
edges[package.declaration.name] = package.spec.dependencies.reject(&:optional).map(&:name)
|
|
11
13
|
end
|
|
12
14
|
cycle = DependencyGraph.find_dependency_cycle(edges)
|
|
13
15
|
return unless cycle
|
|
14
16
|
|
|
15
17
|
raise Error.resolution("dependency cycle detected: #{cycle.join(" -> ")}")
|
|
16
18
|
end
|
|
19
|
+
|
|
20
|
+
def validate_optional_referrals(packages)
|
|
21
|
+
return if packages.all? { |package| package.spec.dependencies.none?(&:optional) }
|
|
22
|
+
|
|
23
|
+
packages_by_name = packages.to_h { |package| [package.declaration.name, package] }
|
|
24
|
+
packages.each do |package|
|
|
25
|
+
package.spec.dependencies.select(&:optional).each do |dependency|
|
|
26
|
+
target = packages_by_name[dependency.name]
|
|
27
|
+
next unless target
|
|
28
|
+
|
|
29
|
+
target.spec.satisfy_constraint!(dependency.constraint)
|
|
30
|
+
rescue Error
|
|
31
|
+
raise Error.resolution(
|
|
32
|
+
"optional referral #{package.declaration.name} -> #{dependency.name} requires " \
|
|
33
|
+
"#{dependency.constraint}, but selected version is #{target.spec.recorded_version}"
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
17
38
|
end
|
|
18
39
|
end
|
data/lib/pray/verify.rb
CHANGED
|
@@ -36,11 +36,16 @@ module Pray
|
|
|
36
36
|
|
|
37
37
|
def drift_project(project, lockfile)
|
|
38
38
|
report, rendered_targets, fresh_targets = collect_verification_report(project, lockfile)
|
|
39
|
-
fresh_targets.each do |path,
|
|
40
|
-
normalized_fresh = Hashing.normalize_line_endings(fresh_content)
|
|
39
|
+
fresh_targets.each do |path, fresh_target|
|
|
41
40
|
on_disk = rendered_targets[path]
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
retired = RenderReconcile.retired_span_checksums(lockfile, fresh_target)
|
|
42
|
+
expected = if on_disk
|
|
43
|
+
RenderReconcile.reconcile_content(on_disk, fresh_target.content, retired)
|
|
44
|
+
else
|
|
45
|
+
fresh_target.content
|
|
46
|
+
end
|
|
47
|
+
normalized_on_disk = Hashing.normalize_line_endings(on_disk) if on_disk
|
|
48
|
+
unless normalized_on_disk == Hashing.normalize_line_endings(expected)
|
|
44
49
|
report.findings << VerificationFinding.new(
|
|
45
50
|
kind: "renderer_drift",
|
|
46
51
|
message: "#{path} differs from fresh render"
|
|
@@ -64,7 +69,7 @@ module Pray
|
|
|
64
69
|
def collect_verification_report(project, lockfile)
|
|
65
70
|
report = VerificationReport.new
|
|
66
71
|
rendered_targets = {}
|
|
67
|
-
fresh_targets = Render.render_project(project).to_h { |target| [target.path, target
|
|
72
|
+
fresh_targets = Render.render_project(project).to_h { |target| [target.path, target] }
|
|
68
73
|
|
|
69
74
|
if project.manifest_hash != lockfile.manifest_hash
|
|
70
75
|
report.findings << VerificationFinding.new(
|
|
@@ -104,6 +109,7 @@ module Pray
|
|
|
104
109
|
end
|
|
105
110
|
|
|
106
111
|
target_spans = lockfile.managed_span.group_by(&:target)
|
|
112
|
+
fresh_targets.each_key { |target_path| target_spans[target_path] ||= [] }
|
|
107
113
|
target_spans.each do |target_path, spans|
|
|
108
114
|
absolute_path = File.join(project.project_root, target_path)
|
|
109
115
|
unless File.exist?(absolute_path)
|
|
@@ -135,7 +141,7 @@ module Pray
|
|
|
135
141
|
end
|
|
136
142
|
end
|
|
137
143
|
end
|
|
138
|
-
fresh_lines = fresh_targets[target_path]&.lines(chomp: true)
|
|
144
|
+
fresh_lines = fresh_targets[target_path]&.content&.lines(chomp: true)
|
|
139
145
|
summary = VerifyPosition.summarize_position_drift(
|
|
140
146
|
target_path,
|
|
141
147
|
spans,
|
data/lib/pray/version.rb
CHANGED
data/lib/pray.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative "pray/format_manifest"
|
|
|
18
18
|
require_relative "pray/package_relation"
|
|
19
19
|
require_relative "pray/package_spec"
|
|
20
20
|
require_relative "pray/package_spec_paths"
|
|
21
|
+
require_relative "pray/package_spec_tree_hash"
|
|
21
22
|
require_relative "pray/package_spec_version"
|
|
22
23
|
require_relative "pray/local_prayer"
|
|
23
24
|
require_relative "pray/lockfile"
|
data/pray-cli.gemspec
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pray-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.26.
|
|
4
|
+
version: 1.26.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -77,14 +77,14 @@ dependencies:
|
|
|
77
77
|
requirements:
|
|
78
78
|
- - "~>"
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '
|
|
80
|
+
version: '4.0'
|
|
81
81
|
type: :development
|
|
82
82
|
prerelease: false
|
|
83
83
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
84
|
requirements:
|
|
85
85
|
- - "~>"
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '
|
|
87
|
+
version: '4.0'
|
|
88
88
|
description: 'Ruby Prayfile CLI and library: resolve, lock, render, verify, and publish
|
|
89
89
|
inference input packages. Consumes path, git, and registry distribution points.'
|
|
90
90
|
email:
|
|
@@ -166,6 +166,7 @@ files:
|
|
|
166
166
|
- lib/pray/package_spec.rb
|
|
167
167
|
- lib/pray/package_spec_paths.rb
|
|
168
168
|
- lib/pray/package_spec_render.rb
|
|
169
|
+
- lib/pray/package_spec_tree_hash.rb
|
|
169
170
|
- lib/pray/package_spec_version.rb
|
|
170
171
|
- lib/pray/path_safety.rb
|
|
171
172
|
- lib/pray/plan.rb
|
|
@@ -251,7 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
251
252
|
- !ruby/object:Gem::Version
|
|
252
253
|
version: '0'
|
|
253
254
|
requirements: []
|
|
254
|
-
rubygems_version:
|
|
255
|
+
rubygems_version: 4.0.20
|
|
255
256
|
specification_version: 4
|
|
256
257
|
summary: Package manager for the language placed before inference (Prayfile)
|
|
257
258
|
test_files: []
|