pray-cli 1.9.1 → 1.10.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/CHANGELOG.md +25 -0
- data/bin/pray +0 -1
- data/lib/pray/archive.rb +18 -16
- data/lib/pray/archive_unpack.rb +68 -0
- data/lib/pray/cli/commands/workflow.rb +1 -1
- data/lib/pray/cli/helpers.rb +2 -1
- data/lib/pray/compose_dest.rb +59 -0
- data/lib/pray/format_serialize.rb +6 -1
- data/lib/pray/http_body.rb +34 -0
- data/lib/pray/lockfile.rb +32 -6
- data/lib/pray/lockfile_serialize.rb +15 -0
- data/lib/pray/manifest.rb +26 -9
- data/lib/pray/manifest_json.rb +4 -4
- data/lib/pray/manifest_parser.rb +1 -0
- data/lib/pray/manifest_parser_blocks.rb +5 -2
- data/lib/pray/manifest_parser_helpers.rb +34 -5
- data/lib/pray/materialize.rb +5 -3
- data/lib/pray/package_spec.rb +19 -0
- data/lib/pray/path_safety.rb +43 -0
- data/lib/pray/plan.rb +5 -11
- data/lib/pray/registry.rb +33 -70
- data/lib/pray/registry_install.rb +30 -0
- data/lib/pray/registry_integrity.rb +63 -0
- data/lib/pray/render.rb +43 -65
- data/lib/pray/render_content.rb +36 -0
- data/lib/pray/render_dest.rb +215 -0
- data/lib/pray/render_layout.rb +21 -0
- data/lib/pray/render_patch.rb +107 -0
- data/lib/pray/resolve.rb +11 -54
- data/lib/pray/resolve_exports.rb +79 -0
- data/lib/pray/resolve_source.rb +28 -0
- data/lib/pray/resource_limits.rb +14 -0
- data/lib/pray/serve.rb +46 -4
- data/lib/pray/session.rb +36 -4
- data/lib/pray/tar_validation.rb +157 -0
- data/lib/pray/verify.rb +2 -0
- data/lib/pray/verify_provisioned.rb +55 -0
- data/lib/pray/version.rb +2 -2
- data/lib/pray.rb +12 -0
- metadata +15 -1
data/lib/pray/package_spec.rb
CHANGED
|
@@ -24,6 +24,25 @@ module Pray
|
|
|
24
24
|
super
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def deprecation_warnings
|
|
28
|
+
keywords = []
|
|
29
|
+
keywords << "spec.skills" unless skills.nil? || skills.empty?
|
|
30
|
+
keywords << "skill" if (exports || {}).values.any? { |export| export.kind == "skill" }
|
|
31
|
+
Manifest.new(deprecated_keywords: keywords).deprecation_warnings
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.warn_resolved_deprecations(packages)
|
|
35
|
+
seen = {}
|
|
36
|
+
packages.each do |package|
|
|
37
|
+
package.spec.deprecation_warnings.each do |warning|
|
|
38
|
+
next if seen[warning]
|
|
39
|
+
|
|
40
|
+
seen[warning] = true
|
|
41
|
+
warn(warning)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
27
46
|
def canonicalized
|
|
28
47
|
dup.tap do |copy|
|
|
29
48
|
copy.files = files.sort
|
data/lib/pray/path_safety.rb
CHANGED
|
@@ -37,5 +37,48 @@ module Pray
|
|
|
37
37
|
|
|
38
38
|
cleaned
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
def validate_archive_member_path!(path)
|
|
42
|
+
cleaned = path.to_s.delete_prefix("./")
|
|
43
|
+
if cleaned.empty? || Pathname.new(cleaned).absolute? || cleaned.start_with?("/")
|
|
44
|
+
raise Error.integrity("package path must be relative: #{path}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
cleaned.split("/").each do |part|
|
|
48
|
+
next if part.empty? || part == "."
|
|
49
|
+
|
|
50
|
+
if part == ".." || part.include?("\0")
|
|
51
|
+
raise Error.integrity("package path escapes package root: #{path}")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
cleaned
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_project_relative_path!(value)
|
|
59
|
+
path = value.to_s.strip
|
|
60
|
+
windows_absolute = path.match?(/\A(?:[A-Za-z]:[\\\/]|[\\\/]{2})/)
|
|
61
|
+
if path.empty? || Pathname.new(path).absolute? || windows_absolute
|
|
62
|
+
raise Error.manifest("project path must be repository-relative: #{value}")
|
|
63
|
+
end
|
|
64
|
+
parts = path.tr("\\", "/").split("/")
|
|
65
|
+
if parts.include?("..") || path.include?("\0")
|
|
66
|
+
raise Error.manifest("project path escapes repository root: #{value}")
|
|
67
|
+
end
|
|
68
|
+
if parts.all? { |part| part.empty? || part == "." }
|
|
69
|
+
raise Error.manifest("project path must be repository-relative: #{value}")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
path
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def validate_destination_path!(value)
|
|
76
|
+
path = value.to_s.strip
|
|
77
|
+
if path.start_with?("~")
|
|
78
|
+
raise Error.manifest("project path must be repository-relative: #{value}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
validate_project_relative_path!(value)
|
|
82
|
+
end
|
|
40
83
|
end
|
|
41
84
|
end
|
data/lib/pray/plan.rb
CHANGED
|
@@ -15,7 +15,9 @@ module Pray
|
|
|
15
15
|
package_lines: package_summary_lines(previous_lockfile, lockfile, project),
|
|
16
16
|
lockfile: lockfile_change_status(lockfile_path, lockfile),
|
|
17
17
|
targets: rendered.map { |target| target_change(project, target) },
|
|
18
|
-
provisioned: Render.planned_provisioned_files(project).map
|
|
18
|
+
provisioned: Render.planned_provisioned_files(project).map do |file|
|
|
19
|
+
provisioned_change(project, file, previous_lockfile)
|
|
20
|
+
end,
|
|
19
21
|
warnings: []
|
|
20
22
|
)
|
|
21
23
|
end
|
|
@@ -68,16 +70,8 @@ module Pray
|
|
|
68
70
|
[target.path, change]
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
def provisioned_change(project, file)
|
|
72
|
-
|
|
73
|
-
change = if !File.exist?(destination)
|
|
74
|
-
"write"
|
|
75
|
-
elsif FileUtils.compare_file(destination, file.source)
|
|
76
|
-
"unchanged"
|
|
77
|
-
else
|
|
78
|
-
"update"
|
|
79
|
-
end
|
|
80
|
-
[file.path, change]
|
|
73
|
+
def provisioned_change(project, file, previous_lockfile)
|
|
74
|
+
[file.path, RenderDest.destination_status(project, file, previous_lockfile).to_s]
|
|
81
75
|
end
|
|
82
76
|
end
|
|
83
77
|
end
|
data/lib/pray/registry.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "uri"
|
|
|
6
6
|
require "fileutils"
|
|
7
7
|
require "pathname"
|
|
8
8
|
require_relative "path_safety"
|
|
9
|
+
require_relative "http_body"
|
|
9
10
|
|
|
10
11
|
module Pray
|
|
11
12
|
RegistryPackageVersion = Struct.new(
|
|
@@ -33,12 +34,15 @@ module Pray
|
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
module Registry
|
|
37
|
+
extend RegistryIntegrity
|
|
38
|
+
|
|
36
39
|
module_function
|
|
37
40
|
|
|
38
41
|
def resolve_registry_package_root(project_root, source_url, declaration, preferred_version: nil, offline: false)
|
|
39
42
|
metadata = fetch_package_metadata(source_url, declaration.name)
|
|
40
43
|
registry_latest_version = registry_latest_version_label(metadata)
|
|
41
44
|
selected = select_package_version(metadata, declaration.constraint, preferred_version)
|
|
45
|
+
require_integrity_fields!(declaration.name, selected)
|
|
42
46
|
cache_directory = registry_cache_directory(
|
|
43
47
|
project_root,
|
|
44
48
|
source_url,
|
|
@@ -57,11 +61,10 @@ module Pray
|
|
|
57
61
|
|
|
58
62
|
raise Error.resolution(offline_package_error(declaration.name, selected.version)) if offline
|
|
59
63
|
|
|
60
|
-
FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
|
|
61
|
-
FileUtils.mkdir_p(cache_directory)
|
|
62
|
-
|
|
63
64
|
artifact_bytes = read_artifact_bytes(source_url, selected.artifact)
|
|
64
|
-
|
|
65
|
+
RegistryInstall.install_artifact_to_cache(
|
|
66
|
+
cache_directory, declaration, selected, artifact_bytes, source_url: source_url
|
|
67
|
+
)
|
|
65
68
|
|
|
66
69
|
RegistryPackageResolution.new(
|
|
67
70
|
root: cache_directory,
|
|
@@ -83,6 +86,7 @@ module Pray
|
|
|
83
86
|
metadata = parse_metadata(File.read(metadata_path, encoding: "UTF-8"))
|
|
84
87
|
registry_latest_version = registry_latest_version_label(metadata)
|
|
85
88
|
selected = select_package_version(metadata, declaration.constraint, preferred_version)
|
|
89
|
+
require_integrity_fields!(declaration.name, selected)
|
|
86
90
|
cache_directory = registry_cache_directory(
|
|
87
91
|
project_root,
|
|
88
92
|
source_key,
|
|
@@ -101,11 +105,10 @@ module Pray
|
|
|
101
105
|
|
|
102
106
|
raise Error.resolution(offline_package_error(declaration.name, selected.version)) if offline
|
|
103
107
|
|
|
104
|
-
FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
|
|
105
|
-
FileUtils.mkdir_p(cache_directory)
|
|
106
|
-
|
|
107
108
|
artifact_bytes = read_local_artifact_bytes(source_root, selected.artifact)
|
|
108
|
-
|
|
109
|
+
RegistryInstall.install_artifact_to_cache(
|
|
110
|
+
cache_directory, declaration, selected, artifact_bytes, source_url: source_root
|
|
111
|
+
)
|
|
109
112
|
|
|
110
113
|
RegistryPackageResolution.new(
|
|
111
114
|
root: cache_directory,
|
|
@@ -177,51 +180,13 @@ module Pray
|
|
|
177
180
|
Gem::Version.new(left) <=> Gem::Version.new(right)
|
|
178
181
|
end
|
|
179
182
|
|
|
180
|
-
def validate_and_unpack(cache_directory, declaration, selected, artifact_bytes, source_url: nil)
|
|
181
|
-
if selected.artifact_hash
|
|
182
|
-
artifact_hash = Hashing.sha256_prefixed(artifact_bytes)
|
|
183
|
-
if artifact_hash != selected.artifact_hash
|
|
184
|
-
raise Error.integrity(
|
|
185
|
-
"package artifact hash mismatch for #{declaration.name} #{selected.version}"
|
|
186
|
-
)
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
verify_registry_signature!(declaration, selected, artifact_bytes)
|
|
191
|
-
Trust.verify_publisher_fingerprint!(source_url, selected) if source_url
|
|
192
|
-
|
|
193
|
-
Archive.unpack_praypkg(artifact_bytes, cache_directory)
|
|
194
|
-
spec_path = Resolve.find_prayspec_file(cache_directory)
|
|
195
|
-
spec = Pray.parse_package_spec(File.read(spec_path)).canonicalized
|
|
196
|
-
if spec.name != declaration.name
|
|
197
|
-
raise Error.resolution(
|
|
198
|
-
"package path #{cache_directory.inspect} declares #{spec.name.inspect}, expected #{declaration.name.inspect}"
|
|
199
|
-
)
|
|
200
|
-
end
|
|
201
|
-
if spec.version != selected.version
|
|
202
|
-
raise Error.resolution(
|
|
203
|
-
"package #{declaration.name} version #{spec.version} does not match registry version #{selected.version}"
|
|
204
|
-
)
|
|
205
|
-
end
|
|
206
|
-
if selected.tree_hash
|
|
207
|
-
actual_tree_hash = spec.tree_hash_for_root(cache_directory)
|
|
208
|
-
if actual_tree_hash != selected.tree_hash
|
|
209
|
-
raise Error.integrity(
|
|
210
|
-
"package tree hash mismatch for #{declaration.name} #{selected.version}"
|
|
211
|
-
)
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
183
|
def read_local_artifact_bytes(source_root, artifact)
|
|
217
|
-
if artifact.start_with?("http://", "https://")
|
|
218
|
-
return http_get(artifact)
|
|
219
|
-
end
|
|
220
184
|
if artifact.start_with?("file://")
|
|
221
185
|
path = PathSafety.join_under_root(source_root, artifact.delete_prefix("file://"))
|
|
222
186
|
raise Error.resolution("package artifact path escapes distribution root") unless path
|
|
223
187
|
return File.binread(path)
|
|
224
188
|
end
|
|
189
|
+
reject_absolute_artifact!(artifact)
|
|
225
190
|
|
|
226
191
|
path = PathSafety.join_under_root(source_root, artifact)
|
|
227
192
|
raise Error.resolution("package artifact path escapes distribution root") unless path
|
|
@@ -233,17 +198,14 @@ module Pray
|
|
|
233
198
|
def read_artifact_bytes(source_url, artifact)
|
|
234
199
|
return read_local_artifact_bytes(source_url, artifact) if local_source?(source_url)
|
|
235
200
|
|
|
201
|
+
reject_absolute_artifact!(artifact)
|
|
236
202
|
http_get(join_url(source_url, artifact))
|
|
237
203
|
end
|
|
238
204
|
|
|
239
|
-
def
|
|
240
|
-
return
|
|
205
|
+
def reject_absolute_artifact!(artifact)
|
|
206
|
+
return unless artifact.match?(%r{\A[a-z][a-z0-9+.-]*:}i)
|
|
241
207
|
|
|
242
|
-
|
|
243
|
-
spec = Pray.parse_package_spec(File.read(spec_path)).canonicalized
|
|
244
|
-
spec.version == selected.version
|
|
245
|
-
rescue Errno::ENOENT, Errno::ENOTDIR, SystemCallError
|
|
246
|
-
false
|
|
208
|
+
raise Error.integrity("remote artifact path must be relative: #{artifact}")
|
|
247
209
|
end
|
|
248
210
|
|
|
249
211
|
def registry_metadata_path(source_root, package_name)
|
|
@@ -301,12 +263,7 @@ module Pray
|
|
|
301
263
|
|
|
302
264
|
def http_get(url)
|
|
303
265
|
uri = URI(url)
|
|
304
|
-
|
|
305
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
306
|
-
raise Error.resolution("HTTP request failed for #{url}: #{response.code}")
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
response.body
|
|
266
|
+
bounded_http_exchange(uri, Net::HTTP::Get.new(uri), "HTTP request failed for #{url}")
|
|
310
267
|
end
|
|
311
268
|
|
|
312
269
|
def http_put(url, content_type, body)
|
|
@@ -314,12 +271,7 @@ module Pray
|
|
|
314
271
|
request = Net::HTTP::Put.new(uri)
|
|
315
272
|
request["Content-Type"] = content_type
|
|
316
273
|
request.body = body
|
|
317
|
-
|
|
318
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
319
|
-
raise Error.resolution("HTTP upload failed for #{url}: #{response.code}")
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
response.body
|
|
274
|
+
bounded_http_exchange(uri, request, "HTTP upload failed for #{url}")
|
|
323
275
|
end
|
|
324
276
|
|
|
325
277
|
def http_post(url, content_type, body)
|
|
@@ -327,12 +279,23 @@ module Pray
|
|
|
327
279
|
request = Net::HTTP::Post.new(uri)
|
|
328
280
|
request["Content-Type"] = content_type
|
|
329
281
|
request.body = body
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
282
|
+
bounded_http_exchange(uri, request, "HTTP request failed for #{url}")
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def bounded_http_exchange(uri, request, failure_message)
|
|
286
|
+
body = nil
|
|
287
|
+
code = nil
|
|
288
|
+
success = false
|
|
289
|
+
http_request(uri) do |http|
|
|
290
|
+
http.request(request) do |response|
|
|
291
|
+
success = response.is_a?(Net::HTTPSuccess)
|
|
292
|
+
code = response.code
|
|
293
|
+
body = HttpBody.read_response!(response)
|
|
294
|
+
end
|
|
333
295
|
end
|
|
296
|
+
raise Error.resolution("#{failure_message}: #{code}") unless success
|
|
334
297
|
|
|
335
|
-
|
|
298
|
+
body
|
|
336
299
|
end
|
|
337
300
|
|
|
338
301
|
def http_request(uri)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Pray
|
|
6
|
+
module RegistryInstall
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def install_artifact_to_cache(cache_directory, declaration, selected, artifact_bytes, source_url: nil)
|
|
10
|
+
staging_directory = "#{cache_directory}.staging"
|
|
11
|
+
FileUtils.rm_rf(staging_directory)
|
|
12
|
+
FileUtils.mkdir_p(staging_directory)
|
|
13
|
+
unpacked_directory = File.join(staging_directory, "unpacked")
|
|
14
|
+
FileUtils.mkdir_p(unpacked_directory)
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
Registry.validate_and_unpack(
|
|
18
|
+
unpacked_directory, declaration, selected, artifact_bytes, source_url: source_url
|
|
19
|
+
)
|
|
20
|
+
FileUtils.rm_rf(cache_directory) if File.exist?(cache_directory)
|
|
21
|
+
FileUtils.mv(unpacked_directory, cache_directory)
|
|
22
|
+
rescue
|
|
23
|
+
FileUtils.rm_rf(staging_directory)
|
|
24
|
+
raise
|
|
25
|
+
else
|
|
26
|
+
FileUtils.rm_rf(staging_directory)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module RegistryIntegrity
|
|
5
|
+
def validate_and_unpack(cache_directory, declaration, selected, artifact_bytes, source_url: nil)
|
|
6
|
+
require_integrity_fields!(declaration.name, selected)
|
|
7
|
+
artifact_hash = Hashing.sha256_prefixed(artifact_bytes)
|
|
8
|
+
if artifact_hash != selected.artifact_hash
|
|
9
|
+
raise Error.integrity(
|
|
10
|
+
"package artifact hash mismatch for #{declaration.name} #{selected.version}"
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
verify_registry_signature!(declaration, selected, artifact_bytes)
|
|
15
|
+
Trust.verify_publisher_fingerprint!(source_url, selected) if source_url
|
|
16
|
+
Archive.unpack_praypkg(artifact_bytes, cache_directory)
|
|
17
|
+
spec_path = Resolve.find_prayspec_file(cache_directory)
|
|
18
|
+
spec = Pray.parse_package_spec(File.read(spec_path)).canonicalized
|
|
19
|
+
validate_package_identity!(cache_directory, declaration, selected, spec)
|
|
20
|
+
actual_tree_hash = spec.tree_hash_for_root(cache_directory)
|
|
21
|
+
if actual_tree_hash != selected.tree_hash
|
|
22
|
+
raise Error.integrity(
|
|
23
|
+
"package tree hash mismatch for #{declaration.name} #{selected.version}"
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def require_integrity_fields!(package_name, selected)
|
|
29
|
+
if selected.artifact_hash.to_s.empty?
|
|
30
|
+
raise Error.integrity("package #{package_name} #{selected.version} is missing artifact_hash")
|
|
31
|
+
end
|
|
32
|
+
if selected.tree_hash.to_s.empty?
|
|
33
|
+
raise Error.integrity("package #{package_name} #{selected.version} is missing tree_hash")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cache_ready?(cache_directory, selected)
|
|
38
|
+
return false unless File.directory?(cache_directory)
|
|
39
|
+
|
|
40
|
+
spec_path = Resolve.find_prayspec_file(cache_directory)
|
|
41
|
+
spec = Pray.parse_package_spec(File.read(spec_path)).canonicalized
|
|
42
|
+
spec.version == selected.version && !selected.tree_hash.to_s.empty? &&
|
|
43
|
+
spec.tree_hash_for_root(cache_directory) == selected.tree_hash
|
|
44
|
+
rescue Error, SystemCallError
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def validate_package_identity!(cache_directory, declaration, selected, spec)
|
|
51
|
+
if spec.name != declaration.name
|
|
52
|
+
raise Error.resolution(
|
|
53
|
+
"package path #{cache_directory.inspect} declares #{spec.name.inspect}, expected #{declaration.name.inspect}"
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
return if spec.version == selected.version
|
|
57
|
+
|
|
58
|
+
raise Error.resolution(
|
|
59
|
+
"package #{declaration.name} version #{spec.version} does not match registry version #{selected.version}"
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/pray/render.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "pathname"
|
|
5
|
+
require_relative "render_content"
|
|
5
6
|
|
|
6
7
|
module Pray
|
|
7
8
|
RenderedTarget = Struct.new(:path, :content, :managed_spans) do
|
|
@@ -18,37 +19,34 @@ module Pray
|
|
|
18
19
|
project.manifest.targets.each do |target|
|
|
19
20
|
output = target.outputs.first
|
|
20
21
|
next unless output
|
|
21
|
-
|
|
22
22
|
rendered << render_target(project, target, output)
|
|
23
23
|
end
|
|
24
24
|
rendered
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def write_rendered_targets(project, rendered)
|
|
27
|
+
def write_rendered_targets(project, rendered, previous_lockfile = nil)
|
|
28
28
|
rendered.each do |target|
|
|
29
|
+
PathSafety.validate_destination_path!(target.path)
|
|
30
|
+
RenderDest.ensure_safe_destination_ancestors!(project.project_root, target.path, target.path)
|
|
29
31
|
path = File.join(project.project_root, target.path)
|
|
30
32
|
FileUtils.mkdir_p(File.dirname(path))
|
|
31
|
-
|
|
33
|
+
RenderDest.ensure_safe_destination_ancestors!(project.project_root, target.path, target.path)
|
|
34
|
+
RenderDest.write_rendered_content(path, target.path, target.content)
|
|
32
35
|
end
|
|
33
|
-
materialize_provisioned_exports(project)
|
|
36
|
+
materialize_provisioned_exports(project, previous_lockfile)
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
def materialize_provisioned_exports(project)
|
|
37
|
-
|
|
38
|
-
planned_provisioned_files(project).each do |file|
|
|
39
|
-
destination = File.join(project.project_root, file.path)
|
|
40
|
-
FileUtils.mkdir_p(File.dirname(destination))
|
|
41
|
-
write_provisioned_file(file.source, destination, symbols)
|
|
42
|
-
end
|
|
39
|
+
def materialize_provisioned_exports(project, previous_lockfile = nil)
|
|
40
|
+
RenderDest.materialize(project, previous_lockfile)
|
|
43
41
|
end
|
|
44
42
|
|
|
45
|
-
def
|
|
43
|
+
def expected_provisioned_bytes(source, symbols)
|
|
46
44
|
bytes = File.binread(source)
|
|
47
|
-
text = bytes.force_encoding(Encoding::UTF_8)
|
|
45
|
+
text = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
48
46
|
if text.valid_encoding?
|
|
49
|
-
|
|
47
|
+
Substitute.substitute_pray_symbols(text, symbols)
|
|
50
48
|
else
|
|
51
|
-
|
|
49
|
+
bytes
|
|
52
50
|
end
|
|
53
51
|
end
|
|
54
52
|
|
|
@@ -70,19 +68,19 @@ module Pray
|
|
|
70
68
|
planned.sort_by(&:path).uniq(&:path)
|
|
71
69
|
end
|
|
72
70
|
|
|
73
|
-
PlannedProvisionedFile = Struct.new(:path, :source)
|
|
71
|
+
PlannedProvisionedFile = Struct.new(:path, :source, :package, :export, keyword_init: true)
|
|
74
72
|
|
|
75
73
|
def render_target(project, target, output)
|
|
74
|
+
ComposeDest.ensure_html_comment_dest!(output)
|
|
76
75
|
if target.scoped && target.mode == "compose"
|
|
77
76
|
return render_scoped_compose(project, target, output)
|
|
78
77
|
end
|
|
79
|
-
|
|
80
78
|
render_legacy_compose(project, target, output)
|
|
81
79
|
end
|
|
82
80
|
|
|
83
81
|
def render_scoped_compose(project, target, output)
|
|
84
82
|
builder = ContentBuilder.new
|
|
85
|
-
append_header(builder, project, output)
|
|
83
|
+
append_header(builder, project, target, output)
|
|
86
84
|
symbols = project.manifest.symbols || {}
|
|
87
85
|
managed_spans = []
|
|
88
86
|
|
|
@@ -99,7 +97,7 @@ module Pray
|
|
|
99
97
|
|
|
100
98
|
def render_legacy_compose(project, target, output)
|
|
101
99
|
builder = ContentBuilder.new
|
|
102
|
-
append_header(builder, project, output)
|
|
100
|
+
append_header(builder, project, target, output)
|
|
103
101
|
|
|
104
102
|
unbound_locals = project.local_files.select do |local|
|
|
105
103
|
declaration = project.manifest.local.find { |entry| entry.path == local.manifest_path }
|
|
@@ -137,16 +135,11 @@ module Pray
|
|
|
137
135
|
RenderedTarget.new(path: output, content: builder.finish, managed_spans: managed_spans)
|
|
138
136
|
end
|
|
139
137
|
|
|
140
|
-
def append_header(builder, project, output)
|
|
141
|
-
|
|
138
|
+
def append_header(builder, project, target, output)
|
|
139
|
+
text = ComposeDest.header_text(target, output, project.manifest.render.header)
|
|
140
|
+
return unless text
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
builder.append_line("<!-- pray:0 ignore-comments -->")
|
|
145
|
-
builder.append_empty_line
|
|
146
|
-
builder.append_line("# Agent context")
|
|
147
|
-
builder.append_empty_line
|
|
148
|
-
builder.append_line("Do not edit managed blocks in `#{output_name}` or provisioned files under `.agents/`.")
|
|
149
|
-
builder.append_line("To change shared guidance, update `Prayfile` and run `pray install`.")
|
|
142
|
+
builder.append_body(text)
|
|
150
143
|
builder.append_empty_line
|
|
151
144
|
end
|
|
152
145
|
|
|
@@ -173,7 +166,10 @@ module Pray
|
|
|
173
166
|
|
|
174
167
|
def append_managed_export(builder, managed_spans, package, export, target, output, symbols)
|
|
175
168
|
body = package.export_bodies[export]
|
|
176
|
-
|
|
169
|
+
unless body
|
|
170
|
+
raise Error.integrity("compose cannot write binary export #{export}; use file: for unmarked bytes") if package.spec.exports[export]&.kind == "file"
|
|
171
|
+
raise Error.render("package #{package.declaration.name} is missing cached export #{export}")
|
|
172
|
+
end
|
|
177
173
|
|
|
178
174
|
body = Substitute.substitute_pray_symbols(body, symbols)
|
|
179
175
|
identifier = Hashing.marker_id("#{package.declaration.name}:#{export}:#{target.name}")
|
|
@@ -198,7 +194,7 @@ module Pray
|
|
|
198
194
|
|
|
199
195
|
def should_inline_export?(package, export_name)
|
|
200
196
|
export = package.spec.exports[export_name]
|
|
201
|
-
export.nil? || export.kind
|
|
197
|
+
export.nil? || %w[fragment file].include?(export.kind)
|
|
202
198
|
end
|
|
203
199
|
|
|
204
200
|
def collect_exact_file_bindings(project, planned)
|
|
@@ -215,7 +211,12 @@ module Pray
|
|
|
215
211
|
source = File.join(package.root, export.path)
|
|
216
212
|
raise Error.render("file export source missing: #{source}") unless File.file?(source)
|
|
217
213
|
|
|
218
|
-
planned << PlannedProvisionedFile.new(
|
|
214
|
+
planned << PlannedProvisionedFile.new(
|
|
215
|
+
path: destination,
|
|
216
|
+
source: source,
|
|
217
|
+
package: package.declaration.name,
|
|
218
|
+
export: export_name
|
|
219
|
+
)
|
|
219
220
|
matched = true
|
|
220
221
|
break
|
|
221
222
|
end
|
|
@@ -241,6 +242,8 @@ module Pray
|
|
|
241
242
|
skill_files,
|
|
242
243
|
[],
|
|
243
244
|
[],
|
|
245
|
+
package.declaration.name,
|
|
246
|
+
skill_name,
|
|
244
247
|
planned
|
|
245
248
|
)
|
|
246
249
|
end
|
|
@@ -274,6 +277,8 @@ module Pray
|
|
|
274
277
|
indexed_files,
|
|
275
278
|
export.only || [],
|
|
276
279
|
export.except || [],
|
|
280
|
+
package.declaration.name,
|
|
281
|
+
export_name,
|
|
277
282
|
planned
|
|
278
283
|
)
|
|
279
284
|
when "file"
|
|
@@ -285,7 +290,9 @@ module Pray
|
|
|
285
290
|
destination = File.join(destination_root, export_name, File.basename(source))
|
|
286
291
|
planned << PlannedProvisionedFile.new(
|
|
287
292
|
path: relative_project_path(project, destination),
|
|
288
|
-
source: source
|
|
293
|
+
source: source,
|
|
294
|
+
package: package.declaration.name,
|
|
295
|
+
export: export_name
|
|
289
296
|
)
|
|
290
297
|
end
|
|
291
298
|
end
|
|
@@ -295,7 +302,7 @@ module Pray
|
|
|
295
302
|
File.basename(export_path.delete_suffix("/")).empty? ? export_name : File.basename(export_path.delete_suffix("/"))
|
|
296
303
|
end
|
|
297
304
|
|
|
298
|
-
def collect_tree_files(project, source_root, destination_root, relative_files, only, except, planned)
|
|
305
|
+
def collect_tree_files(project, source_root, destination_root, relative_files, only, except, package_name, export_name, planned)
|
|
299
306
|
raise Error.render("folder source directory missing: #{source_root}") unless File.directory?(source_root)
|
|
300
307
|
raise Error.render("no files listed in package manifest for #{source_root}") if relative_files.empty?
|
|
301
308
|
|
|
@@ -310,7 +317,9 @@ module Pray
|
|
|
310
317
|
destination = File.join(destination_root, relative)
|
|
311
318
|
planned << PlannedProvisionedFile.new(
|
|
312
319
|
path: relative_project_path(project, destination),
|
|
313
|
-
source: source
|
|
320
|
+
source: source,
|
|
321
|
+
package: package_name,
|
|
322
|
+
export: export_name
|
|
314
323
|
)
|
|
315
324
|
matched = true
|
|
316
325
|
end
|
|
@@ -325,36 +334,5 @@ module Pray
|
|
|
325
334
|
rescue ArgumentError
|
|
326
335
|
absolute
|
|
327
336
|
end
|
|
328
|
-
|
|
329
|
-
class ContentBuilder
|
|
330
|
-
def initialize
|
|
331
|
-
@content = +""
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def next_line_number
|
|
335
|
-
@content.count("\n") + 1
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def append_line(line)
|
|
339
|
-
@content << line << "\n"
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
def append_empty_line
|
|
343
|
-
@content << "\n"
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
def append_body(body)
|
|
347
|
-
trimmed = body.sub(/\n+\z/, "")
|
|
348
|
-
return if trimmed.empty?
|
|
349
|
-
|
|
350
|
-
trimmed.each_line(chomp: true) { |line| append_line(line) }
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def finish
|
|
354
|
-
@content.sub(/\n\n+\z/, "\n")
|
|
355
|
-
@content << "\n" unless @content.end_with?("\n")
|
|
356
|
-
@content
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
337
|
end
|
|
360
338
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pray
|
|
4
|
+
module Render
|
|
5
|
+
class ContentBuilder
|
|
6
|
+
def initialize
|
|
7
|
+
@content = +""
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def next_line_number
|
|
11
|
+
@content.count("\n") + 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def append_line(line)
|
|
15
|
+
@content << line << "\n"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def append_empty_line
|
|
19
|
+
@content << "\n"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def append_body(body)
|
|
23
|
+
trimmed = body.sub(/\n+\z/, "")
|
|
24
|
+
return if trimmed.empty?
|
|
25
|
+
|
|
26
|
+
trimmed.each_line(chomp: true) { |line| append_line(line) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def finish
|
|
30
|
+
@content.sub(/\n\n+\z/, "\n")
|
|
31
|
+
@content << "\n" unless @content.end_with?("\n")
|
|
32
|
+
@content
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|